From 0ad6aee38961ba55aab047369a28f54a7ff163d4 Mon Sep 17 00:00:00 2001 From: romanb Date: Sat, 24 May 2008 18:18:37 +0000 Subject: [PATCH] Merged DQL parser branch to trunk. --- lib/Doctrine/Connection.php | 9 +- lib/Doctrine/Connection/Mock.php | 2 +- lib/Doctrine/Connection/Pgsql.php | 2 +- lib/Doctrine/Formatter.php | 2 - lib/Doctrine/Hydrator.php | 69 +- lib/Doctrine/Hydrator/Abstract.php | 122 +- lib/Doctrine/Query.php | 1948 +++------------ lib/Doctrine/Query/Abstract.php | 2173 +++++------------ lib/Doctrine/Query/AbstractResult.php | 266 ++ lib/Doctrine/Query/Abstract_old.php | 1805 ++++++++++++++ lib/Doctrine/Query/CacheHandler.php | 156 ++ lib/Doctrine/Query/Check.php | 169 -- lib/Doctrine/Query/Condition.php | 118 - lib/Doctrine/Query/From.php | 88 - lib/Doctrine/Query/Having.php | 102 - lib/Doctrine/Query/JoinCondition.php | 114 - lib/Doctrine/Query/Parser.php | 358 ++- .../{Offset.php => Parser/Exception.php} | 74 +- lib/Doctrine/Query/ParserResult.php | 86 + lib/Doctrine/Query/Printer.php | 95 + lib/Doctrine/Query/Production.php | 224 ++ .../Query/Production/AggregateExpression.php | 84 + lib/Doctrine/Query/Production/Atom.php | 95 + .../Query/Production/BetweenExpression.php | 68 + .../Query/Production/ComparisonExpression.php | 75 + .../Query/Production/ComparisonOperator.php | 81 + .../Production/ConditionalExpression.php | 72 + .../Query/Production/ConditionalFactor.php | 63 + .../Query/Production/ConditionalPrimary.php | 101 + .../Query/Production/ConditionalTerm.php | 72 + .../{Part.php => Production/DeleteClause.php} | 117 +- .../Query/Production/DeleteStatement.php | 59 + .../Query/Production/ExistsExpression.php | 54 + lib/Doctrine/Query/Production/Expression.php | 79 + lib/Doctrine/Query/Production/Factor.php | 65 + lib/Doctrine/Query/Production/FromClause.php | 74 + lib/Doctrine/Query/Production/Function.php | 79 + .../Query/Production/GroupByClause.php | 69 + .../GroupByItem.php} | 79 +- .../Query/Production/HavingClause.php | 52 + .../Production/IdentificationVariable.php | 64 + .../IdentificationVariableDeclaration.php | 95 + .../Query/Production/InExpression.php | 90 + lib/Doctrine/Query/Production/IndexBy.php | 108 + lib/Doctrine/Query/Production/Join.php | 79 + .../Query/Production/LikeExpression.php | 72 + lib/Doctrine/Query/Production/LimitClause.php | 48 + .../Production/NullComparisonExpression.php | 58 + .../Query/Production/OffsetClause.php | 57 + .../Query/Production/OrderByClause.php | 68 + lib/Doctrine/Query/Production/OrderByItem.php | 59 + .../Query/Production/PathExpression.php | 148 ++ .../PathExpressionEndingWithAsterisk.php | 106 + lib/Doctrine/Query/Production/Primary.php | 85 + .../Query/Production/QuantifiedExpression.php | 72 + .../Query/Production/QueryLanguage.php | 57 + .../Production/RangeVariableDeclaration.php | 237 ++ .../Query/Production/SelectClause.php | 88 + .../Query/Production/SelectExpression.php | 126 + .../Query/Production/SelectStatement.php | 96 + .../SimpleConditionalExpression.php | 109 + .../Query/Production/SimpleSelectClause.php | 68 + lib/Doctrine/Query/Production/Subselect.php | 105 + lib/Doctrine/Query/Production/Term.php | 79 + .../Query/Production/UpdateClause.php | 77 + lib/Doctrine/Query/Production/UpdateItem.php | 62 + .../Query/Production/UpdateStatement.php | 59 + .../Query/Production/VariableDeclaration.php | 138 ++ .../WhereClause.php} | 104 +- lib/Doctrine/Query/ProductionParamHolder.php | 89 + .../Query/{Orderby.php => QueryResult.php} | 101 +- lib/Doctrine/Query/Scanner.php | 219 ++ lib/Doctrine/Query/SqlBuilder.php | 87 + lib/Doctrine/Query/SqlBuilder/Mysql.php | 38 + .../{Limit.php => SqlBuilder/Sqlite.php} | 75 +- lib/Doctrine/Query/SqlExecutor/Abstract.php | 115 + .../Query/SqlExecutor/MultiTableDelete.php | 40 + .../Query/SqlExecutor/MultiTableUpdate.php | 44 + .../Query/SqlExecutor/SingleSelect.php | 45 + .../SqlExecutor/SingleTableDeleteUpdate.php | 46 + lib/Doctrine/Query/Token.php | 151 ++ lib/Doctrine/Query/Where.php | 180 -- lib/Doctrine/Query_old.php | 590 +++++ lib/Doctrine/Relation.php | 16 +- tests/Orm/AllTests.php | 6 +- tests/Orm/Component/AllTests.php | 2 +- tests/Orm/Query/AllTests.php | 41 + tests/Orm/Query/DeleteSqlGenerationTest.php | 270 ++ tests/Orm/Query/DqlGenerationTest.php | 249 ++ tests/Orm/Query/IdentifierRecognitionTest.php | 124 + tests/Orm/Query/LanguageRecognitionTest.php | 345 +++ tests/Orm/Query/ScannerTest.php | 280 +++ tests/Orm/Query/SelectSqlGenerationTest.php | 74 + .../Orm/Query/UpdateSqlGenerationTest.php | 113 +- tests/lib/Doctrine_OrmTestCase.php | 6 +- tests/models/cms/CmsArticle.php | 0 tests/models/cms/CmsComment.php | 0 tests/models/cms/CmsPhonenumber.php | 0 tests/models/forum/ForumBoard.php | 0 tests/models/forum/ForumCategory.php | 0 100 files changed, 11027 insertions(+), 4353 deletions(-) mode change 100644 => 100755 lib/Doctrine/Query.php mode change 100644 => 100755 lib/Doctrine/Query/Abstract.php create mode 100755 lib/Doctrine/Query/AbstractResult.php create mode 100644 lib/Doctrine/Query/Abstract_old.php create mode 100755 lib/Doctrine/Query/CacheHandler.php delete mode 100644 lib/Doctrine/Query/Check.php delete mode 100644 lib/Doctrine/Query/Condition.php delete mode 100644 lib/Doctrine/Query/From.php delete mode 100644 lib/Doctrine/Query/Having.php delete mode 100644 lib/Doctrine/Query/JoinCondition.php rename lib/Doctrine/Query/{Offset.php => Parser/Exception.php} (75%) create mode 100755 lib/Doctrine/Query/ParserResult.php create mode 100644 lib/Doctrine/Query/Printer.php create mode 100644 lib/Doctrine/Query/Production.php create mode 100644 lib/Doctrine/Query/Production/AggregateExpression.php create mode 100644 lib/Doctrine/Query/Production/Atom.php create mode 100644 lib/Doctrine/Query/Production/BetweenExpression.php create mode 100644 lib/Doctrine/Query/Production/ComparisonExpression.php create mode 100644 lib/Doctrine/Query/Production/ComparisonOperator.php create mode 100644 lib/Doctrine/Query/Production/ConditionalExpression.php create mode 100644 lib/Doctrine/Query/Production/ConditionalFactor.php create mode 100644 lib/Doctrine/Query/Production/ConditionalPrimary.php create mode 100644 lib/Doctrine/Query/Production/ConditionalTerm.php rename lib/Doctrine/Query/{Part.php => Production/DeleteClause.php} (57%) create mode 100644 lib/Doctrine/Query/Production/DeleteStatement.php create mode 100644 lib/Doctrine/Query/Production/ExistsExpression.php create mode 100644 lib/Doctrine/Query/Production/Expression.php create mode 100644 lib/Doctrine/Query/Production/Factor.php create mode 100644 lib/Doctrine/Query/Production/FromClause.php create mode 100644 lib/Doctrine/Query/Production/Function.php create mode 100644 lib/Doctrine/Query/Production/GroupByClause.php rename lib/Doctrine/Query/{Select.php => Production/GroupByItem.php} (73%) create mode 100644 lib/Doctrine/Query/Production/HavingClause.php create mode 100644 lib/Doctrine/Query/Production/IdentificationVariable.php create mode 100644 lib/Doctrine/Query/Production/IdentificationVariableDeclaration.php create mode 100644 lib/Doctrine/Query/Production/InExpression.php create mode 100644 lib/Doctrine/Query/Production/IndexBy.php create mode 100644 lib/Doctrine/Query/Production/Join.php create mode 100644 lib/Doctrine/Query/Production/LikeExpression.php create mode 100644 lib/Doctrine/Query/Production/LimitClause.php create mode 100755 lib/Doctrine/Query/Production/NullComparisonExpression.php create mode 100644 lib/Doctrine/Query/Production/OffsetClause.php create mode 100644 lib/Doctrine/Query/Production/OrderByClause.php create mode 100644 lib/Doctrine/Query/Production/OrderByItem.php create mode 100644 lib/Doctrine/Query/Production/PathExpression.php create mode 100644 lib/Doctrine/Query/Production/PathExpressionEndingWithAsterisk.php create mode 100644 lib/Doctrine/Query/Production/Primary.php create mode 100644 lib/Doctrine/Query/Production/QuantifiedExpression.php create mode 100644 lib/Doctrine/Query/Production/QueryLanguage.php create mode 100644 lib/Doctrine/Query/Production/RangeVariableDeclaration.php create mode 100644 lib/Doctrine/Query/Production/SelectClause.php create mode 100644 lib/Doctrine/Query/Production/SelectExpression.php create mode 100644 lib/Doctrine/Query/Production/SelectStatement.php create mode 100644 lib/Doctrine/Query/Production/SimpleConditionalExpression.php create mode 100644 lib/Doctrine/Query/Production/SimpleSelectClause.php create mode 100644 lib/Doctrine/Query/Production/Subselect.php create mode 100644 lib/Doctrine/Query/Production/Term.php create mode 100644 lib/Doctrine/Query/Production/UpdateClause.php create mode 100644 lib/Doctrine/Query/Production/UpdateItem.php create mode 100644 lib/Doctrine/Query/Production/UpdateStatement.php create mode 100644 lib/Doctrine/Query/Production/VariableDeclaration.php rename lib/Doctrine/Query/{Groupby.php => Production/WhereClause.php} (63%) create mode 100644 lib/Doctrine/Query/ProductionParamHolder.php rename lib/Doctrine/Query/{Orderby.php => QueryResult.php} (65%) mode change 100644 => 100755 create mode 100644 lib/Doctrine/Query/Scanner.php create mode 100755 lib/Doctrine/Query/SqlBuilder.php create mode 100755 lib/Doctrine/Query/SqlBuilder/Mysql.php rename lib/Doctrine/Query/{Limit.php => SqlBuilder/Sqlite.php} (75%) mode change 100644 => 100755 create mode 100644 lib/Doctrine/Query/SqlExecutor/Abstract.php create mode 100644 lib/Doctrine/Query/SqlExecutor/MultiTableDelete.php create mode 100644 lib/Doctrine/Query/SqlExecutor/MultiTableUpdate.php create mode 100644 lib/Doctrine/Query/SqlExecutor/SingleSelect.php create mode 100644 lib/Doctrine/Query/SqlExecutor/SingleTableDeleteUpdate.php create mode 100644 lib/Doctrine/Query/Token.php delete mode 100644 lib/Doctrine/Query/Where.php create mode 100644 lib/Doctrine/Query_old.php create mode 100755 tests/Orm/Query/AllTests.php create mode 100755 tests/Orm/Query/DeleteSqlGenerationTest.php create mode 100755 tests/Orm/Query/DqlGenerationTest.php create mode 100755 tests/Orm/Query/IdentifierRecognitionTest.php create mode 100755 tests/Orm/Query/LanguageRecognitionTest.php create mode 100755 tests/Orm/Query/ScannerTest.php create mode 100755 tests/Orm/Query/SelectSqlGenerationTest.php rename lib/Doctrine/Query/Set.php => tests/Orm/Query/UpdateSqlGenerationTest.php (54%) mode change 100644 => 100755 mode change 100644 => 100755 tests/models/cms/CmsArticle.php mode change 100644 => 100755 tests/models/cms/CmsComment.php mode change 100644 => 100755 tests/models/cms/CmsPhonenumber.php mode change 100644 => 100755 tests/models/forum/ForumBoard.php mode change 100644 => 100755 tests/models/forum/ForumCategory.php diff --git a/lib/Doctrine/Connection.php b/lib/Doctrine/Connection.php index 8dfe6b87b..1403bfbfd 100644 --- a/lib/Doctrine/Connection.php +++ b/lib/Doctrine/Connection.php @@ -303,7 +303,8 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun */ public function getDbh() { - $this->connect(); + //$this->connect(); + return $this->dbh; } @@ -319,8 +320,8 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun return false; } - $event = new Doctrine_Event($this, Doctrine_Event::CONN_CONNECT); - $this->getListener()->preConnect($event); + //$event = new Doctrine_Event($this, Doctrine_Event::CONN_CONNECT); + //$this->getListener()->preConnect($event); $e = explode(':', $this->options['dsn']); if (extension_loaded('pdo')) { @@ -346,7 +347,7 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun $this->isConnected = true; - $this->getListener()->postConnect($event); + //$this->getListener()->postConnect($event); return true; } diff --git a/lib/Doctrine/Connection/Mock.php b/lib/Doctrine/Connection/Mock.php index 33613114b..775f0b25a 100644 --- a/lib/Doctrine/Connection/Mock.php +++ b/lib/Doctrine/Connection/Mock.php @@ -36,7 +36,7 @@ class Doctrine_Connection_Mock extends Doctrine_Connection_Common /** * @var string $driverName the name of this connection driver */ - protected $driverName = 'Mock'; + protected $driverName = 'MySql'; /** * the constructor diff --git a/lib/Doctrine/Connection/Pgsql.php b/lib/Doctrine/Connection/Pgsql.php index a350409c5..0b969042f 100644 --- a/lib/Doctrine/Connection/Pgsql.php +++ b/lib/Doctrine/Connection/Pgsql.php @@ -106,7 +106,7 @@ class Doctrine_Connection_Pgsql extends Doctrine_Connection_Common { if (is_array($item)) { foreach ($item as $key => $value) { - if (is_bool($value)) { + if (is_bool($value) || is_numeric($item)) { $item[$key] = ($value) ? 'true' : 'false'; } } diff --git a/lib/Doctrine/Formatter.php b/lib/Doctrine/Formatter.php index 8aa44d7b2..6a029ae7d 100644 --- a/lib/Doctrine/Formatter.php +++ b/lib/Doctrine/Formatter.php @@ -168,8 +168,6 @@ class Doctrine_Formatter extends Doctrine_Connection_Module case 'gzip': case 'blob': case 'clob': - $this->conn->connect(); - return $this->conn->getDbh()->quote($input); } } diff --git a/lib/Doctrine/Hydrator.php b/lib/Doctrine/Hydrator.php index 239f166da..3a97aad47 100644 --- a/lib/Doctrine/Hydrator.php +++ b/lib/Doctrine/Hydrator.php @@ -33,10 +33,9 @@ * @author Roman Borschel */ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract -{ +{ /** * hydrateResultSet - * parses the data returned by statement object * * This is method defines the core of Doctrine's object population algorithm. * @@ -46,7 +45,6 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract * @todo: Detailed documentation. Refactor (too long & nesting level). * * @param mixed $stmt - * @param array $tableAliases Array that maps table aliases (SQL alias => DQL alias) * @param array $aliasMap Array that maps DQL aliases to their components * (DQL alias => array( * 'table' => Table object, @@ -71,7 +69,7 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract if ($hydrationMode == Doctrine::HYDRATE_NONE) { return $stmt->fetchAll(PDO::FETCH_NUM); } - + $this->_tableAliases = $parserResult->getTableToClassAliasMap(); $this->_queryComponents = $parserResult->getQueryComponents(); @@ -87,24 +85,29 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract // Used variables during hydration reset($this->_queryComponents); + $rootAlias = key($this->_queryComponents); $rootComponentName = $this->_queryComponents[$rootAlias]['table']->getComponentName(); // if only one component is involved we can make our lives easier $isSimpleQuery = count($this->_queryComponents) <= 1; + // Holds hydration listeners that get called during hydration $listeners = array(); + // Lookup map to quickly discover/lookup existing records in the result // It's the identifier "memory" $identifierMap = array(); + // Holds for each component the last previously seen element in the result set $prev = array(); - // holds the values of the identifier/primary key fields of components, + + // Holds the values of the identifier/primary key fields of components, // separated by a pipe '|' and grouped by component alias (r, u, i, ... whatever) // the $idTemplate is a prepared template. $id is set to a fresh template when // starting to process a row. $id = array(); $idTemplate = array(); - + // Holds the resulting hydrated data structure $result = $driver->getElementCollection($rootComponentName); @@ -122,9 +125,10 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract $prev[$dqlAlias] = array(); $idTemplate[$dqlAlias] = ''; } - + // Process result set $cache = array(); + while ($data = $stmt->fetch(Doctrine::FETCH_ASSOC)) { $id = $idTemplate; // initialize the id-memory $nonemptyComponents = array(); @@ -138,16 +142,16 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract // just event stuff $event->set('data', $rowData[$rootAlias]); + $listeners[$componentName]->preHydrate($event); - //-- - - // Check for an existing element + $index = false; if ($isSimpleQuery || ! isset($identifierMap[$rootAlias][$id[$rootAlias]])) { $element = $driver->getElement($rowData[$rootAlias], $componentName); // just event stuff $event->set('data', $element); + $listeners[$componentName]->postHydrate($event); //-- @@ -158,6 +162,7 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract } else if ( ! isset($element[$field])) { throw new Doctrine_Hydrator_Exception("Couldn't hydrate. Found a non-existent key."); } + $result[$element[$field]] = $element; } else { $result[] = $element; @@ -170,10 +175,8 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract $this->_setLastElement($prev, $result, $index, $rootAlias, false); unset($rowData[$rootAlias]); - - // end hydrate data of the root component for the current row - - + // End hydrate data of the root component for the current row + // $prev[$rootAlias] now points to the last element in $result. // now hydrate the rest of the data found in the current row, that belongs to other // (related) components. @@ -184,6 +187,7 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract // just event stuff $event->set('data', $data); + $listeners[$componentName]->preHydrate($event); //-- @@ -219,6 +223,7 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract } else if ( ! isset($element[$field])) { throw Doctrine_Hydrator_Exception::nonExistantFieldUsedAsIndex($field); } + $prev[$parent][$relationAlias][$element[$field]] = $element; } else { $prev[$parent][$relationAlias][] = $element; @@ -236,6 +241,7 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract } else { // 1-1 relation $oneToOne = true; + if ( ! isset($nonemptyComponents[$dqlAlias])) { $prev[$parent][$relationAlias] = $driver->getNullPointer(); } else if ( ! isset($prev[$parent][$relationAlias])) { @@ -251,14 +257,13 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract } $stmt->closeCursor(); - $driver->flush(); - - // re-enable lazy loading + + // Re-enable lazy loading foreach ($this->_queryComponents as $dqlAlias => $data) { $data['table']->setAttribute(Doctrine::ATTR_LOAD_REFERENCES, true); } - + //$e = microtime(true); //echo 'Hydration took: ' . ($e - $s) . ' for '.count($result).' records
'; @@ -284,14 +289,14 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract if ($coll === $this->_nullObject) { return false; } - + if ($index !== false) { // Link element at $index to previous element for the component // identified by the DQL alias $alias $prev[$dqlAlias] =& $coll[$index]; return; } - + if (is_array($coll) && $coll) { if ($oneToOne) { $prev[$dqlAlias] =& $coll; @@ -307,19 +312,22 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract unset($prev[$dqlAlias]); } } - + + /** + * _gatherRowData + * * Puts the fields of a data row into a new array, grouped by the component - * they belong to. The column names in the result set are mapped to their + * they belong to. The column names in the result set are mapped to their * field names during this procedure. * - * @return array An array with all the fields (name => value) of the data row, + * @return array An array with all the fields (name => value) of the data row, * grouped by their component (alias). */ protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents) { $rowData = array(); - + foreach ($data as $key => $value) { // Parse each column name only once. Cache the results. if ( ! isset($cache[$key])) { @@ -374,13 +382,16 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract $nonemptyComponents[$dqlAlias] = true; } } - + return $rowData; } - - /** + + + /** + * _getCustomIndexField + * * Gets the custom field used for indexing for the specified component alias. - * + * * @return string The field name of the field used for indexing or NULL * if the component does not use any custom field indices. */ @@ -388,5 +399,5 @@ class Doctrine_Hydrator extends Doctrine_Hydrator_Abstract { return isset($this->_queryComponents[$alias]['map']) ? $this->_queryComponents[$alias]['map'] : null; } - + } diff --git a/lib/Doctrine/Hydrator/Abstract.php b/lib/Doctrine/Hydrator/Abstract.php index ce7558f11..83c39e49d 100644 --- a/lib/Doctrine/Hydrator/Abstract.php +++ b/lib/Doctrine/Hydrator/Abstract.php @@ -33,22 +33,23 @@ abstract class Doctrine_Hydrator_Abstract { /** - * @var array $_aliasMap two dimensional array containing the map for query aliases - * Main keys are component aliases + * @var array $_queryComponents * - * table table object associated with given alias + * Two dimensional array containing the map for query aliases. Main keys are component aliases. * - * relation the relation object owned by the parent - * - * parent the alias of the parent - * - * agg the aggregates of this component - * - * map the name of the column / aggregate value this - * component is mapped to a collection + * table Table object associated with given alias. + * relation Relation object owned by the parent. + * parent Alias of the parent. + * agg Aggregates of this component. + * map Name of the column / aggregate value this component is mapped to a collection. */ protected $_queryComponents = array(); + /** + * @var array Table alias map. Keys are SQL aliases and values DQL aliases. + */ + protected $_tableAliasMap = array(); + /** * The current hydration mode. */ @@ -58,6 +59,7 @@ abstract class Doctrine_Hydrator_Abstract protected $_em; + /** * constructor * @@ -69,96 +71,88 @@ abstract class Doctrine_Hydrator_Abstract $this->_nullObject = Doctrine_Null::$INSTANCE; } + /** - * Sets the fetchmode. + * setHydrationMode * - * @param integer $fetchmode One of the Doctrine::HYDRATE_* constants. + * Defines the hydration process mode. + * + * @param integer $hydrationMode Doctrine processing mode to be used during hydration process. + * One of the Doctrine::HYDRATE_* constants. */ public function setHydrationMode($hydrationMode) { $this->_hydrationMode = $hydrationMode; } + /** - * setAliasMap - * sets the whole component alias map + * setQueryComponents * - * @param array $map alias map - * @return Doctrine_Hydrate this object + * Defines the mapping components. + * + * @param array $queryComponents Query components. */ public function setQueryComponents(array $queryComponents) { $this->_queryComponents = $queryComponents; } + /** - * getAliasMap - * returns the component alias map + * getQueryComponents * - * @return array component alias map + * Gets the mapping components. + * + * @return array Query components. */ public function getQueryComponents() { return $this->_queryComponents; } - - /** - * hasAliasDeclaration - * whether or not this object has a declaration for given component alias - * - * @param string $componentAlias the component alias the retrieve the declaration from - * @return boolean - */ - public function hasAliasDeclaration($componentAlias) - { - return isset($this->_queryComponents[$componentAlias]); - } - - /** - * getAliasDeclaration - * get the declaration for given component alias - * - * @param string $componentAlias the component alias the retrieve the declaration from - * @return array the alias declaration - * @deprecated - */ - public function getAliasDeclaration($componentAlias) - { - return $this->getQueryComponent($componentAlias); - } + /** - * getQueryComponent - * get the declaration for given component alias + * setTableAliasMap * - * @param string $componentAlias the component alias the retrieve the declaration from - * @return array the alias declaration + * Defines the table aliases. + * + * @param array $tableAliasMap Table aliases. */ - public function getQueryComponent($componentAlias) + public function setTableAliasMap(array $tableAliasMap) { - if ( ! isset($this->_queryComponents[$componentAlias])) { - throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); - } - - return $this->_queryComponents[$componentAlias]; + $this->_tableAliasMap = $tableAliasMap; } + /** - * parseData - * parses the data returned by statement object + * getTableAliasMap + * + * Returns all table aliases. + * + * @return array Table aliases as an array. + */ + public function getTableAliasMap() + { + return $this->_tableAliasMap; + } + + + /** + * hydrateResultSet + * + * Processes data returned by statement object. * * This is method defines the core of Doctrine object population algorithm - * hence this method strives to be as fast as possible + * hence this method strives to be as fast as possible. * * The key idea is the loop over the rowset only once doing all the needed operations * within this massive loop. * - * @todo: Can we refactor this function so that it is not so long and - * nested? - * - * @param mixed $stmt - * @return array + * @param mixed $stmt PDOStatement + * @param integer $hydrationMode Doctrine processing mode to be used during hydration process. + * One of the Doctrine::HYDRATE_* constants. + * @return mixed Doctrine_Collection|array */ abstract public function hydrateResultSet($parserResult); - } diff --git a/lib/Doctrine/Query.php b/lib/Doctrine/Query.php old mode 100644 new mode 100755 index d2b3bd0df..6950039c6 --- a/lib/Doctrine/Query.php +++ b/lib/Doctrine/Query.php @@ -1,6 +1,7 @@ * @author Konsta Vesterinen - * @todo Proposal: This class does far too much. It should have only 1 task: Collecting - * the DQL query parts and the query parameters (the query state and caching options/methods - * can remain here, too). - * The actual SQL construction could be done by a separate object (Doctrine_Query_SqlBuilder?) - * whose task it is to convert DQL into SQL. - * Furthermore the SqlBuilder? can then use other objects (Doctrine_Query_Tokenizer?), - * (Doctrine_Query_Parser(s)?) to accomplish his work. Doctrine_Query does not need - * to know the tokenizer/parsers. There could be extending - * implementations of SqlBuilder? that cover the specific SQL dialects. - * This would release Doctrine_Connection and the Doctrine_Connection_xxx classes - * from this tedious task. - * This would also largely reduce the currently huge interface of Doctrine_Query(_Abstract) - * and better hide all these transformation internals from the public Query API. - * - * @internal The lifecycle of a Query object is the following: - * After construction the query object is empty. Through using the fluent - * query interface the user fills the query object with DQL parts and query parameters. - * These get collected in {@link $_dqlParts} and {@link $_params}, respectively. - * When the query is executed the first time, or when {@link getSqlQuery()} - * is called the first time, the collected DQL parts get parsed and the resulting - * connection-driver specific SQL is generated. The generated SQL parts are - * stored in {@link $_sqlParts} and the final resulting SQL query is stored in - * {@link $_sql}. */ -class Doctrine_Query extends Doctrine_Query_Abstract implements Countable, Serializable +class Doctrine_Query extends Doctrine_Query_Abstract { /** - * @var array The DQL keywords. + * @var Doctrine_Connection The connection used by this query object. */ - protected static $_keywords = array('ALL', - 'AND', - 'ANY', - 'AS', - 'ASC', - 'AVG', - 'BETWEEN', - 'BIT_LENGTH', - 'BY', - 'CHARACTER_LENGTH', - 'CHAR_LENGTH', - 'CURRENT_DATE', - 'CURRENT_TIME', - 'CURRENT_TIMESTAMP', - 'DELETE', - 'DESC', - 'DISTINCT', - 'EMPTY', - 'EXISTS', - 'FALSE', - 'FETCH', - 'FROM', - 'GROUP', - 'HAVING', - 'IN', - 'INDEXBY', - 'INNER', - 'IS', - 'JOIN', - 'LEFT', - 'LIKE', - 'LOWER', - 'MEMBER', - 'MOD', - 'NEW', - 'NOT', - 'NULL', - 'OBJECT', - 'OF', - 'OR', - 'ORDER', - 'OUTER', - 'POSITION', - 'SELECT', - 'SOME', - 'TRIM', - 'TRUE', - 'UNKNOWN', - 'UPDATE', - 'WHERE'); - - /** - * @var array - */ - protected $_subqueryAliases = array(); - - /** - * @var array $_aggregateAliasMap an array containing all aggregate aliases, keys as dql aliases - * and values as sql aliases - */ - protected $_aggregateAliasMap = array(); - - /** - * @var array - */ - protected $_pendingAggregates = array(); + protected $_connection; /** - * @param boolean $needsSubquery + * @var Doctrine_Hydrator The hydrator object used to hydrate query results. */ - protected $_needsSubquery = false; + protected $_hydrator; /** - * @param boolean $isSubquery whether or not this query object is a subquery of another - * query object + * @var Doctrine_Query_ParserResult The parser result that holds DQL => SQL information. */ - protected $_isSubquery; + protected $_parserResult; /** - * @var array $_neededTables an array containing the needed table aliases + * @var string $_sql Cached SQL query. */ - protected $_neededTables = array(); + protected $_sql = null; + + + // Caching Stuff /** - * @var array $pendingSubqueries SELECT part subqueries, these are called pending subqueries since - * they cannot be parsed directly (some queries might be correlated) + * @var Doctrine_Cache_Interface The cache driver used for caching result sets. */ - protected $_pendingSubqueries = array(); + protected $_resultCache; /** - * @var array $_pendingFields an array of pending fields (fields waiting to be parsed) + * @var boolean Boolean value that indicates whether or not expire the result cache. */ - protected $_pendingFields = array(); + protected $_expireResultCache = false; /** - * @var array $_parsers an array of parser objects, each DQL query part has its own parser + * @var int Result Cache lifetime. */ - protected $_parsers = array(); + protected $_resultCacheTTL; + /** - * @var array $_pendingJoinConditions an array containing pending joins + * @var Doctrine_Cache_Interface The cache driver used for caching queries. */ - protected $_pendingJoinConditions = array(); - - /** - * @var array - */ - protected $_expressionMap = array(); - - /** - * @var string $_sql cached SQL query - */ - protected $_sql; - + protected $_queryCache; /** - * create - * returns a new Doctrine_Query object + * @var boolean Boolean value that indicates whether or not expire the query cache. + */ + protected $_expireQueryCache = false; + + /** + * @var int Query Cache lifetime. + */ + protected $_queryCacheTTL; + + // End of Caching Stuff + + + public function __construct(Doctrine_Connection $conn = null, Doctrine_Hydrator_Abstract $hydrator = null) + { + $this->setConnection($conn); + + if ($hydrator === null) { + $hydrator = new Doctrine_Hydrator(Doctrine_EntityManager::getManager()); + } + + $this->_hydrator = $hydrator; + + $this->free(); + } + + + /** + * Returns a new Doctrine_Query object * * @param Doctrine_Connection $conn optional connection parameter * @return Doctrine_Query */ public static function create($conn = null) { - return new Doctrine_Query($conn); - } - - /** - * Resets the query to the state just after it has been instantiated. - */ - public function reset() - { - $this->_pendingJoinConditions = array(); - $this->_pendingSubqueries = array(); - $this->_pendingFields = array(); - $this->_neededTables = array(); - $this->_expressionMap = array(); - $this->_subqueryAliases = array(); - $this->_needsSubquery = false; - $this->_isLimitSubqueryUsed = false; + return new self($conn); } + /** - * createSubquery - * creates a subquery + * Retrieves the assocated Doctrine_Connection to this Doctrine_Query * - * @return Doctrine_Hydrate + * @return Doctrine_Connection */ - public function createSubquery() + public function getConnection() { - $class = get_class($this); - $obj = new $class(); - - // copy the aliases to the subquery - $obj->copyAliases($this); - - // this prevents the 'id' being selected, re ticket #307 - $obj->isSubquery(true); - - return $obj; + return $this->_connection; } - /** - * _addPendingJoinCondition - * - * @param string $componentAlias component alias - * @param string $joinCondition dql join condition - * @return Doctrine_Query this object - */ - protected function _addPendingJoinCondition($componentAlias, $joinCondition) - { - $this->_pendingJoins[$componentAlias] = $joinCondition; - } /** - * addEnumParam - * sets input parameter as an enumerated parameter + * Defines an assocated Doctrine_Connection to this Doctrine_Query * - * @param string $key the key of the input parameter - * @return Doctrine_Query + * @param Doctrine_Connection $conn A valid Doctrine_Connection + * @return void */ - public function addEnumParam($key, $table = null, $column = null) + public function setConnection(Doctrine_Connection $conn = null) { - $array = (isset($table) || isset($column)) ? array($table, $column) : array(); - - if ($key === '?') { - $this->_enumParams[] = $array; - } else { - $this->_enumParams[$key] = $array; + if ($conn === null) { + $conn = Doctrine_EntityManager::getManager()->getConnection(); } + + $this->_connection = $conn; } - /** - * getEnumParams - * get all enumerated parameters - * - * @return array all enumerated parameters - */ - public function getEnumParams() - { - return $this->_enumParams; - } - - /** - * getDql - * returns the DQL query that is represented by this query object. - * - * the query is built from $_dqlParts - * - * @return string the DQL query - */ - public function getDql() - { - $q = ''; - $q .= ( ! empty($this->_dqlParts['select']))? 'SELECT ' . implode(', ', $this->_dqlParts['select']) : ''; - $q .= ( ! empty($this->_dqlParts['from']))? ' FROM ' . implode(' ', $this->_dqlParts['from']) : ''; - $q .= ( ! empty($this->_dqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_dqlParts['where']) : ''; - $q .= ( ! empty($this->_dqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_dqlParts['groupby']) : ''; - $q .= ( ! empty($this->_dqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_dqlParts['having']) : ''; - $q .= ( ! empty($this->_dqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_dqlParts['orderby']) : ''; - $q .= ( ! empty($this->_dqlParts['limit']))? ' LIMIT ' . implode(' ', $this->_dqlParts['limit']) : ''; - $q .= ( ! empty($this->_dqlParts['offset']))? ' OFFSET ' . implode(' ', $this->_dqlParts['offset']) : ''; - return $q; - } - /** - * getParams + * Returns the hydrator associated with this query object * - * @return array + * @return Doctrine_Hydrator The hydrator associated with this query object */ - public function getParams() + public function getHydrator() { - return array_merge($this->_params['join'], $this->_params['set'], $this->_params['where'], $this->_params['having']); + return $this->_hydrator; } - + + /** - * setParams - * - * @param array $params - */ - public function setParams(array $params = array()) { - $this->_params = $params; - } - - /** - * fetchArray * Convenience method to execute using array fetching as hydration mode. * * @param string $params @@ -316,9 +166,9 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable, Seria public function fetchArray($params = array()) { return $this->execute($params, Doctrine::HYDRATE_ARRAY); } - + + /** - * fetchOne * Convenience method to execute the query and return the first item * of the collection. * @@ -343,681 +193,376 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable, Seria return false; } + /** - * isSubquery - * if $bool parameter is set this method sets the value of - * Doctrine_Query::$isSubquery. If this value is set to true - * the query object will not load the primary key fields of the selected - * components. + * Query the database with DQL (Doctrine Query Language). * - * If null is given as the first parameter this method retrieves the current - * value of Doctrine_Query::$isSubquery. - * - * @param boolean $bool whether or not this query acts as a subquery - * @return Doctrine_Query|bool + * @param string $query DQL query + * @param array $params prepared statement parameters + * @param int $hydrationMode Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD + * @see Doctrine::FETCH_* constants + * @return mixed */ - public function isSubquery($bool = null) + public function query($query, $params = array(), $hydrationMode = null) { - if ($bool === null) { - return $this->_isSubquery; + $this->setDql($query); + return $this->execute($params, $hydrationMode); + } + + + /** + * Builds the sql query from the given parameters and applies things such as + * column aggregation inheritance and limit subqueries if needed + * + * @return mixed The built sql query or an array of all sql queries. + */ + public function getSql() + { + return $this->parse()->getSqlExecutor()->getSqlStatements(); + } + + + /** + * Parses the DQL query, if necessary, and stores the parser result. + * + * @return Doctrine_Query_ParserResult + */ + public function parse() + { + if ($this->_state === self::STATE_DIRTY) { + $parser = new Doctrine_Query_Parser($this->getDql()); + $this->_parserResult = $parser->parse(); + $this->_state = self::STATE_CLEAN; } - $this->_isSubquery = (bool) $bool; + return $this->_parserResult; + } + + + /** + * Executes the query and populates the data set. + * + * @param string $params Parameters to be sent to query. + * @param integer $hydrationMode Doctrine processing mode to be used during hydration process. + * One of the Doctrine::HYDRATE_* constants. + * @return Doctrine_Collection The root collection + */ + public function execute($params = array(), $hydrationMode = null) + { + $params = $this->getParams($params); + + // If there is a CacheDriver associated to cache resultsets... + if ($this->_resultCache && $this->_type === self::SELECT) { // Only executes if "SELECT" + $cacheDriver = $this->getResultCacheDriver(); + + // Calculate hash for dql query. + $hash = md5($this->getDql() . var_export($params, true)); + $cached = ($this->_expireResultCache) ? false : $cacheDriver->fetch($hash); + + if ($cached === false) { + // Cache does not exist, we have to create it. + $result = $this->_execute($params, Doctrine::HYDRATE_ARRAY); + $queryResult = Doctrine_Query_CacheHandler::fromResultSet($this, $result); + $cacheDriver->save($hash, $queryResult->toCachedForm(), $this->_resultCacheTTL); + + return $result; + } else { + // Cache exists, recover it and return the results. + $queryResult = Doctrine_Query_CacheHandler::fromCachedResult($this, $cached); + + return $queryResult->getResultSet(); + } + } + + return $this->_execute($params, $hydrationMode); + } + + + /** + * _execute + * + * @param string $params Parameters to be sent to query. + * @param int $hydrationMode Method of hydration to be used. + * @return Doctrine_Collection The root collection + */ + protected function _execute($params, $hydrationMode) + { + // preQuery invoking + $this->preQuery(); + + // Query execution + $stmt = $this->_execute2($params); + + // postQuery invoking + $this->postQuery(); + + if (is_integer($stmt)) { + return $stmt; + } + + return $this->_hydrator->hydrateResultSet($stmt, $hydrationMode); + } + + + /** + * _execute2 + * + * @param array $params + * @return PDOStatement The executed PDOStatement. + */ + protected function _execute2($params) + { + // If there is a CacheDriver associated to cache queries... + if ($this->_queryCache || $this->_connection->getAttribute(Doctrine::ATTR_QUERY_CACHE)) { + $queryCacheDriver = $this->getQueryCacheDriver(); + + // Calculate hash for dql query. + $hash = md5($this->getDql() . 'DOCTRINE_QUERY_CACHE_SALT'); + $cached = ($this->_expireQueryCache) ? false : $queryCacheDriver->fetch($hash); + + if ($cached === false) { + // Cache does not exist, we have to create it. + $executor = $this->parse()->getSqlExecutor(); + + // To-be cached item is parserResult + $cacheDriver->save($hash, $this->_parserResult->toCachedForm(), $this->_queryCacheTTL); + } else { + // Cache exists, recover it and return the results. + $this->_parserResult = Doctrine_Query_CacheHandler::fromCachedQuery($this, $cached); + + $executor = $this->_parserResult->getSqlExecutor(); + } + } else { + $executor = $this->parse()->getSqlExecutor(); + } + + // Assignments for Hydrator and Enums + $this->_hydrator->setQueryComponents($this->_parserResult->getQueryComponents()); + $this->_hydrator->setTableAliasMap($this->_parserResult->getTableAliasMap()); + $this->_setEnumParams($this->_parserResult->getEnumParams()); + + // Converting parameters + $params = $this->_prepareParams($params); + + // Double the params if we are using limit-subquery algorithm + // We always have an instance of Doctrine_Query_ParserResult on hands... + if ($this->_parserResult->isLimitSubqueryUsed() && + $this->_connection->getAttribute(Doctrine::ATTR_DRIVER_NAME) !== 'mysql') { + $params = array_merge($params, $params); + } + + // Executing the query and assigning PDOStatement + return $executor->execute($this->_conn, $params); + } + + + /** + * @nodoc + */ + protected function _prepareParams(array $params) + { + // Convert boolean params + $params = $this->_connection->convertBooleans($params); + + // Convert enum params + return $this->convertEnums($params); + } + + + /** + * Defines a cache driver to be used for caching result sets. + * + * @param Doctrine_Cache_Interface|null $driver Cache driver + * @return Doctrine_Query + */ + public function setResultCache($resultCache) + { + if ($resultCache !== null && ! ($resultCache instanceof Doctrine_Cache_Interface)) { + throw new Doctrine_Query_Exception( + 'Method setResultCache() accepts only an instance of Doctrine_Cache_Interface or null.' + ); + } + + $this->_resultCache = $resultCache; + return $this; } - /** - * getAggregateAlias - * - * @param string $dqlAlias the dql alias of an aggregate value - * @return string - * @deprecated - */ - public function getAggregateAlias($dqlAlias) - { - return $this->getSqlAggregateAlias($dqlAlias); - } - - /** - * getSqlAggregateAlias - * - * @param string $dqlAlias the dql alias of an aggregate value - * @return string - */ - public function getSqlAggregateAlias($dqlAlias) - { - if (isset($this->_aggregateAliasMap[$dqlAlias])) { - // mark the expression as used - $this->_expressionMap[$dqlAlias][1] = true; - return $this->_aggregateAliasMap[$dqlAlias]; - } else if ( ! empty($this->_pendingAggregates)) { - $this->processPendingAggregates(); - - return $this->getSqlAggregateAlias($dqlAlias); + /** + * Returns the cache driver used for caching result sets. + * + * @return Doctrine_Cache_Interface Cache driver + */ + public function getResultCache() + { + if ($this->_resultCache instanceof Doctrine_Cache_Interface) { + return $this->_resultCache; } else { - throw new Doctrine_Query_Exception('Unknown aggregate alias: ' . $dqlAlias); + return $this->_connection->getResultCacheDriver(); } } - + + /** - * getDqlPart - * returns a specific DQL query part. + * Defines how long the result cache will be active before expire. * - * @param string $queryPart the name of the query part - * @return string the DQL query part - * @todo Description: List which query parts exist or point to the method/property - * where they are listed. + * @param integer $timeToLive How long the cache entry is valid + * @return Doctrine_Query */ - public function getDqlPart($queryPart) + public function setResultCacheLifetime($timeToLive) { - if ( ! isset($this->_dqlParts[$queryPart])) { - throw new Doctrine_Query_Exception('Unknown query part ' . $queryPart); + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; } - return $this->_dqlParts[$queryPart]; + $this->_resultCacheTTL = $timeToLive; + + return $this; } + /** - * contains + * Retrieves the lifetime of resultset cache. * - * Method to check if a arbitrary piece of dql exists - * - * @param string $dql Arbitrary piece of dql to check for - * @return boolean + * @return int */ - public function contains($dql) + public function getResultCacheLifetime() { - return stripos($this->getDql(), $dql) === false ? false : true; + return $this->_resultCacheTTL; } - /** - * processPendingFields - * the fields in SELECT clause cannot be parsed until the components - * in FROM clause are parsed, hence this method is called everytime a - * specific component is being parsed. - * - * @throws Doctrine_Query_Exception if unknown component alias has been given - * @param string $componentAlias the alias of the component - * @return void - * @todo Description: What is a 'pending field' (and are there non-pending fields, too)? - * What is 'processed'? (Meaning: What information is gathered & stored away) - */ - public function processPendingFields($componentAlias) - { - $tableAlias = $this->getSqlTableAlias($componentAlias); - $baseTable = $this->_queryComponents[$componentAlias]['table']; - $mapper = $this->_queryComponents[$componentAlias]['mapper']; - if ( ! isset($this->_pendingFields[$componentAlias])) { - return; + /** + * Defines if the resultset cache is active or not. + * + * @param boolean $expire Whether or not to force resultset cache expiration. + * @return Doctrine_Query + */ + public function setExpireResultCache($expire = true) + { + $this->_expireResultCache = (bool) $expire; + + return $this; + } + + + /** + * Retrieves if the resultset cache is active or not. + * + * @return bool + */ + public function getExpireResultCache() + { + return $this->_expireResultCache; + } + + + /** + * Defines a cache driver to be used for caching queries. + * + * @param Doctrine_Cache_Interface|null $driver Cache driver + * @return Doctrine_Query + */ + public function setQueryCache($queryCache) + { + if ($queryCache !== null && ! ($queryCache instanceof Doctrine_Cache_Interface)) { + throw new Doctrine_Query_Exception( + 'Method setResultCache() accepts only an instance of Doctrine_Cache_Interface or null.' + ); } - $fields = $this->_pendingFields[$componentAlias]; + $this->_queryCache = $queryCache; - // check for wildcards - if (in_array('*', $fields)) { - $fields = $mapper->getFieldNames(); + return $this; + } + + + /** + * Returns the cache driver used for caching queries. + * + * @return Doctrine_Cache_Interface Cache driver + */ + public function getQueryCache() + { + if ($this->_queryCache instanceof Doctrine_Cache_Interface) { + return $this->_queryCache; } else { - // only auto-add the primary key fields if this query object is not - // a subquery of another query object - if ( ! $this->_isSubquery) { - $fields = array_unique(array_merge((array) $baseTable->getIdentifier(), $fields)); - } + return $this->_connection->getQueryCacheDriver(); } - - $fields = array_unique(array_merge($fields, $mapper->getCustomFields())); - - $sql = array(); - foreach ($fields as $fieldName) { - $table = $mapper->getOwningClass($fieldName); - if ($table !== $baseTable) { - $tableAlias = $this->getSqlTableAlias($componentAlias . '.' . $table->getComponentName()); - } else { - $tableAlias = $this->getSqlTableAlias($componentAlias); - } - - $columnName = $table->getColumnName($fieldName); - $columnName = $table->getColumnName($fieldName); - $sql[] = $this->_conn->quoteIdentifier($tableAlias . '.' . $columnName) - . ' AS ' - . $this->_conn->quoteIdentifier($this->getSqlTableAlias($componentAlias) . '__' . $columnName); - - if ( ! in_array($tableAlias, $this->_neededTables)) { - $this->_neededTables[] = $tableAlias; - } - } - - return implode(', ', $sql); } + /** - * parseSelectField + * Defines how long the query cache will be active before expire. * - * @throws Doctrine_Query_Exception if unknown component alias has been given - * @return void - * @todo Description: Explain what this method does. Is there a relation to parseSelect()? - * (It doesnt seem to get called from there...?). In what circumstances is this method - * used? + * @param integer $timeToLive How long the cache entry is valid + * @return Doctrine_Query */ - public function parseSelectField($field) + public function setQueryCacheLifetime($timeToLive) { - $terms = explode('.', $field); - - if (isset($terms[1])) { - $componentAlias = $terms[0]; - $field = $terms[1]; - } else { - reset($this->_queryComponents); - $componentAlias = key($this->_queryComponents); - $fields = $terms[0]; + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; } - $tableAlias = $this->getTableAlias($componentAlias); - $table = $this->_queryComponents[$componentAlias]['table']; + $this->_queryCacheTTL = $timeToLive; - - // check for wildcards - if ($field === '*') { - $sql = array(); - - foreach ($table->getColumnNames() as $field) { - $sql[] = $this->parseSelectField($componentAlias . '.' . $field); - } - - return implode(', ', $sql); - } else { - $name = $table->getColumnName($field); - - $this->_neededTables[] = $tableAlias; - - return $this->_conn->quoteIdentifier($tableAlias . '.' . $name) - . ' AS ' - . $this->_conn->quoteIdentifier($tableAlias . '__' . $name); - } + return $this; } + /** - * getExpressionOwner - * returns the component alias for owner of given expression + * Retrieves the lifetime of resultset cache. * - * @param string $expr expression from which to get to owner from - * @return string the component alias - * @todo Description: What does it mean if a component is an 'owner' of an expression? - * What kind of 'expression' are we talking about here? + * @return int */ - public function getExpressionOwner($expr) + public function getQueryCacheLifetime() { - if (strtoupper(substr(trim($expr, '( '), 0, 6)) !== 'SELECT') { - preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $expr, $matches); - - $match = current($matches); - - if (isset($match[0])) { - $terms = explode('.', $match[0]); - - return $terms[0]; - } - } - return $this->getRootAlias(); - + return $this->_queryCacheTTL; } + /** - * parseSelect - * parses the query select part and - * adds selected fields to pendingFields array + * Defines if the query cache is active or not. * - * @param string $dql - * @todo Description: What information is extracted (and then stored)? + * @param boolean $expire Whether or not to force query cache expiration. + * @return Doctrine_Query */ - public function parseSelect($dql) + public function setExpireQueryCache($expire = true) { - $refs = $this->_tokenizer->sqlExplode($dql, ','); + $this->_expireQueryCache = (bool) $expire; - $pos = strpos(trim($refs[0]), ' '); - $first = substr($refs[0], 0, $pos); - - // check for DISTINCT keyword - if ($first === 'DISTINCT') { - $this->_sqlParts['distinct'] = true; - $refs[0] = substr($refs[0], ++$pos); - } - - $parsedComponents = array(); - - foreach ($refs as $reference) { - $reference = trim($reference); - - if (empty($reference)) { - continue; - } - - $terms = $this->_tokenizer->sqlExplode($reference, ' '); - - $pos = strpos($terms[0], '('); - - if (count($terms) > 1 || $pos !== false) { - $expression = array_shift($terms); - $alias = array_pop($terms); - - if ( ! $alias) { - $alias = substr($expression, 0, $pos); - } - - $componentAlias = $this->getExpressionOwner($expression); - $expression = $this->parseClause($expression); - - $tableAlias = $this->getTableAlias($componentAlias); - - $index = count($this->_aggregateAliasMap); - - $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); - - $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; - - $this->_aggregateAliasMap[$alias] = $sqlAlias; - $this->_expressionMap[$alias][0] = $expression; - - $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; - - $this->_neededTables[] = $tableAlias; - } else { - $e = explode('.', $terms[0]); - - if (isset($e[1])) { - $componentAlias = $e[0]; - $field = $e[1]; - } else { - reset($this->_queryComponents); - $componentAlias = key($this->_queryComponents); - $field = $e[0]; - } - $this->_pendingFields[$componentAlias][] = $field; - } - } + return $this; } + /** - * parseClause - * parses given DQL clause + * Retrieves if the query cache is active or not. * - * this method handles five tasks: - * - * 1. Converts all DQL functions to their native SQL equivalents - * 2. Converts all component references to their table alias equivalents - * 3. Converts all field names to actual column names - * 4. Quotes all identifiers - * 5. Parses nested clauses and subqueries recursively - * - * @return string SQL string - * @todo Description: What is a 'dql clause' (and what not)? - * Refactor: Too long & nesting level + * @return bool */ - public function parseClause($clause) + public function getExpireQueryCache() { - $clause = trim($clause); - - if (is_numeric($clause)) { - return $clause; - } - - $terms = $this->_tokenizer->clauseExplode($clause, array(' ', '+', '-', '*', '/')); - - $str = ''; - foreach ($terms as $term) { - $pos = strpos($term[0], '('); - - if ($pos !== false) { - $name = substr($term[0], 0, $pos); - $term[0] = $this->parseFunctionExpression($term[0]); - } else { - if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'") { - - if (strpos($term[0], '.') !== false) { - if ( ! is_numeric($term[0])) { - $e = explode('.', $term[0]); - - $field = array_pop($e); - - if ($this->getType() === Doctrine_Query::SELECT) { - $componentAlias = implode('.', $e); - - if (empty($componentAlias)) { - $componentAlias = $this->getRootAlias(); - } - - $this->load($componentAlias); - - // check the existence of the component alias - if ( ! isset($this->_queryComponents[$componentAlias])) { - throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); - } - - $table = $this->_queryComponents[$componentAlias]['table']; - - $def = $table->getDefinitionOf($field); - - // get the actual field name from alias - $field = $table->getColumnName($field); - - // check column existence - if ( ! $def) { - throw new Doctrine_Query_Exception('Unknown column ' . $field); - } - - if (isset($def['owner'])) { - $componentAlias = $componentAlias . '.' . $def['owner']; - } - - $tableAlias = $this->getTableAlias($componentAlias); - - // build sql expression - $term[0] = $this->_conn->quoteIdentifier($tableAlias) - . '.' - . $this->_conn->quoteIdentifier($field); - } else { - // build sql expression - $field = $this->getRoot()->getColumnName($field); - $term[0] = $this->_conn->quoteIdentifier($field); - } - } - } else { - if ( ! empty($term[0]) && - ! in_array(strtoupper($term[0]), self::$_keywords) && - ! is_numeric($term[0])) { - - $componentAlias = $this->getRootAlias(); - - $found = false; - - if ($componentAlias !== false && - $componentAlias !== null) { - $table = $this->_queryComponents[$componentAlias]['table']; - - // check column existence - if ($table->hasField($term[0])) { - $found = true; - - $def = $table->getDefinitionOf($term[0]); - - // get the actual column name from field name - $term[0] = $table->getColumnName($term[0]); - - - if (isset($def['owner'])) { - $componentAlias = $componentAlias . '.' . $def['owner']; - } - - $tableAlias = $this->getTableAlias($componentAlias); - - if ($this->getType() === Doctrine_Query::SELECT) { - // build sql expression - $term[0] = $this->_conn->quoteIdentifier($tableAlias) - . '.' - . $this->_conn->quoteIdentifier($term[0]); - } else { - // build sql expression - $term[0] = $this->_conn->quoteIdentifier($term[0]); - } - } else { - $found = false; - } - } - - if ( ! $found) { - $term[0] = $this->getSqlAggregateAlias($term[0]); - } - } - } - } - } - - $str .= $term[0] . $term[1]; - } - return $str; - } - - public function parseIdentifierReference($expr) - { - + return $this->_expireQueryCache; } - public function parseFunctionExpression($expr) - { - $pos = strpos($expr, '('); - - $name = substr($expr, 0, $pos); - if ($name === '') { - return $this->parseSubquery($expr); - } - - $argStr = substr($expr, ($pos + 1), -1); - - $args = array(); - // parse args - - foreach ($this->_tokenizer->sqlExplode($argStr, ',') as $arg) { - $args[] = $this->parseClause($arg); - } - - // convert DQL function to its RDBMS specific equivalent - try { - $expr = call_user_func_array(array($this->_conn->expression, $name), $args); - } catch (Doctrine_Expression_Exception $e) { - throw new Doctrine_Query_Exception('Unknown function ' . $name . '.'); - } - - return $expr; - } - public function parseSubquery($subquery) - { - $trimmed = trim($this->_tokenizer->bracketTrim($subquery)); - - // check for possible subqueries - if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { - // parse subquery - $trimmed = $this->createSubquery()->parseDqlQuery($trimmed)->getQuery(); - } else { - // parse normal clause - $trimmed = $this->parseClause($trimmed); - } - - return '(' . $trimmed . ')'; - } /** - * processPendingSubqueries - * processes pending subqueries + * Defines the processing mode to be used during hydration process. * - * subqueries can only be processed when the query is fully constructed - * since some subqueries may be correlated - * - * @return void - * @todo Better description. i.e. What is a 'pending subquery'? What does 'processed' mean? - * (parsed? sql is constructed? some information is gathered?) + * @param integer $hydrationMode Doctrine processing mode to be used during hydration process. + * One of the Doctrine::HYDRATE_* constants. + * @return Doctrine_Query */ - public function processPendingSubqueries() + public function setHydrationMode($hydrationMode) { - foreach ($this->_pendingSubqueries as $value) { - list($dql, $alias) = $value; + $this->_hydrator->setHydrationMode($hydrationMode); - $subquery = $this->createSubquery(); - - $sql = $subquery->parseDqlQuery($dql, false)->getQuery(); - - reset($this->_queryComponents); - $componentAlias = key($this->_queryComponents); - $tableAlias = $this->getTableAlias($componentAlias); - - $sqlAlias = $tableAlias . '__' . count($this->_aggregateAliasMap); - - $this->_sqlParts['select'][] = '(' . $sql . ') AS ' . $this->_conn->quoteIdentifier($sqlAlias); - - $this->_aggregateAliasMap[$alias] = $sqlAlias; - $this->_queryComponents[$componentAlias]['agg'][] = $alias; - } - $this->_pendingSubqueries = array(); + return $this; } - /** - * processPendingAggregates - * processes pending aggregate values for given component alias - * - * @return void - * @todo Better description. i.e. What is a 'pending aggregate'? What does 'processed' mean? - */ - public function processPendingAggregates() - { - // iterate trhough all aggregates - foreach ($this->_pendingAggregates as $aggregate) { - list ($expression, $components, $alias) = $aggregate; - - $tableAliases = array(); - - // iterate through the component references within the aggregate function - if ( ! empty ($components)) { - foreach ($components as $component) { - - if (is_numeric($component)) { - continue; - } - - $e = explode('.', $component); - - $field = array_pop($e); - $componentAlias = implode('.', $e); - - // check the existence of the component alias - if ( ! isset($this->_queryComponents[$componentAlias])) { - throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); - } - - $table = $this->_queryComponents[$componentAlias]['table']; - - $field = $table->getColumnName($field); - - // check column existence - if ( ! $table->hasColumn($field)) { - throw new Doctrine_Query_Exception('Unknown column ' . $field); - } - - $sqlTableAlias = $this->getSqlTableAlias($componentAlias); - - $tableAliases[$sqlTableAlias] = true; - - // build sql expression - - $identifier = $this->_conn->quoteIdentifier($sqlTableAlias . '.' . $field); - $expression = str_replace($component, $identifier, $expression); - } - } - - if (count($tableAliases) !== 1) { - $componentAlias = reset($this->_tableAliasMap); - $tableAlias = key($this->_tableAliasMap); - } - - $index = count($this->_aggregateAliasMap); - $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); - - $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; - - $this->_aggregateAliasMap[$alias] = $sqlAlias; - $this->_expressionMap[$alias][0] = $expression; - - $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; - - $this->_neededTables[] = $tableAlias; - } - // reset the state - $this->_pendingAggregates = array(); - } /** - * _buildSqlQueryBase - * returns the base of the generated sql query - * On mysql driver special strategy has to be used for DELETE statements - * (where is this special strategy??) - * - * @return string the base of the generated sql query - */ - protected function _buildSqlQueryBase() - { - switch ($this->_type) { - case self::DELETE: - $q = 'DELETE FROM '; - break; - case self::UPDATE: - $q = 'UPDATE '; - break; - case self::SELECT: - $distinct = ($this->_sqlParts['distinct']) ? 'DISTINCT ' : ''; - $q = 'SELECT ' . $distinct . implode(', ', $this->_sqlParts['select']) . ' FROM '; - break; - } - return $q; - } - - /** - * _buildSqlFromPart - * builds the from part of the query and returns it - * - * @return string the query sql from part - */ - protected function _buildSqlFromPart() - { - $q = ''; - foreach ($this->_sqlParts['from'] as $k => $part) { - if ($k === 0) { - $q .= $part; - continue; - } - - // preserve LEFT JOINs only if needed - // Check if it's JOIN, if not add a comma separator instead of space - if (!preg_match('/\bJOIN\b/i', $part) && !isset($this->_pendingJoinConditions[$k])) { - $q .= ', ' . $part; - } else { - if (substr($part, 0, 9) === 'LEFT JOIN') { - $e = explode(' ', $part); - - $aliases = array_merge($this->_subqueryAliases, - array_keys($this->_neededTables)); - - if ( ! in_array($e[3], $aliases) && - ! in_array($e[2], $aliases) && - - ! empty($this->_pendingFields)) { - continue; - } - - } - - if (isset($this->_pendingJoinConditions[$k])) { - $parser = new Doctrine_Query_JoinCondition($this, $this->_tokenizer); - - if (strpos($part, ' ON ') !== false) { - $part .= ' AND '; - } else { - $part .= ' ON '; - } - $part .= $parser->parse($this->_pendingJoinConditions[$k]); - - unset($this->_pendingJoinConditions[$k]); - } - - $q .= ' ' . $part; - } - - $this->_sqlParts['from'][$k] = $part; - } - return $q; - } - - /** - * preQuery - * * Empty template method to provide Query subclasses with the possibility * to hook into the query building procedure, doing any custom / specialized * query building procedures that are neccessary. @@ -1030,8 +575,6 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable, Seria } /** - * postQuery - * * Empty template method to provide Query subclasses with the possibility * to hook into the query building procedure, doing any custom / specialized * post query procedures (for example logging) that are neccessary. @@ -1043,749 +586,11 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable, Seria } - /** - * builds the sql query from the given parameters and applies things such as - * column aggregation inheritance and limit subqueries if needed - * - * @param array $params an array of prepared statement params (needed only in mysql driver - * when limit subquery algorithm is used) - * @return string the built sql query - */ - public function getSqlQuery($params = array()) - { - if ($this->_state !== self::STATE_DIRTY) { - return $this->_sql; - } - - // reset the state - if ( ! $this->isSubquery()) { - $this->_queryComponents = array(); - $this->_pendingAggregates = array(); - $this->_aggregateAliasMap = array(); - } - $this->reset(); - - // invoke the preQuery hook - $this->preQuery(); - - // process the DQL parts => generate the SQL parts. - // this will also populate the $_queryComponents. - foreach ($this->_dqlParts as $queryPartName => $queryParts) { - $this->_processDqlQueryPart($queryPartName, $queryParts); - } - $this->_state = self::STATE_CLEAN; - - $params = $this->convertEnums($params); - - // Proceed with the generated SQL - - if (empty($this->_sqlParts['from'])) { - return false; - } - - $needsSubQuery = false; - $subquery = ''; - $map = reset($this->_queryComponents); - $table = $map['table']; - $rootAlias = key($this->_queryComponents); - - if ( ! empty($this->_sqlParts['limit']) && $this->_needsSubquery && - $table->getAttribute(Doctrine::ATTR_QUERY_LIMIT) == Doctrine::LIMIT_RECORDS) { - $this->_isLimitSubqueryUsed = true; - $needsSubQuery = true; - } - - $sql = array(); - foreach ($this->_queryComponents as $alias => $map) { - $fieldSql = $this->processPendingFields($alias); - if ( ! empty($fieldSql)) { - $sql[] = $fieldSql; - } - } - if ( ! empty($sql)) { - array_unshift($this->_sqlParts['select'], implode(', ', $sql)); - } - - $this->_pendingFields = array(); - - // build the basic query - $q = $this->_buildSqlQueryBase(); - $q .= $this->_buildSqlFromPart(); - - if ( ! empty($this->_sqlParts['set'])) { - $q .= ' SET ' . implode(', ', $this->_sqlParts['set']); - } - - // append discriminator column conditions (if any) - $string = $this->_createDiscriminatorConditionSql(); - //echo "orig:$string

"; - if ( ! empty($string)) { - if (substr($string, 0, 1) === '(' && substr($string, -1) === ')') { - $this->_sqlParts['where'][] = $string; - } else { - $this->_sqlParts['where'][] = '(' . $string . ')'; - } - } - - $modifyLimit = true; - if ( ! empty($this->_sqlParts['limit']) || ! empty($this->_sqlParts['offset'])) { - if ($needsSubQuery) { - $subquery = $this->getLimitSubquery(); - // what about composite keys? - $idFieldNames = (array)$table->getIdentifier(); - $idColumnName = $table->getColumnName($idFieldNames[0]); - switch (strtolower($this->_conn->getDriverName())) { - case 'mysql': - // mysql doesn't support LIMIT in subqueries - $list = $this->_conn->execute($subquery, $params)->fetchAll(Doctrine::FETCH_COLUMN); - $subquery = implode(', ', array_map(array($this->_conn, 'quote'), $list)); - break; - case 'pgsql': - // pgsql needs special nested LIMIT subquery - $subquery = 'SELECT doctrine_subquery_alias.' . $idColumnName . ' FROM (' . $subquery . ') AS doctrine_subquery_alias'; - break; - } - - $field = $this->getSqlTableAlias($rootAlias) . '.' . $idColumnName; - - // only append the subquery if it actually contains something - if ($subquery !== '') { - array_unshift($this->_sqlParts['where'], $this->_conn->quoteIdentifier($field) . ' IN (' . $subquery . ')'); - } - - $modifyLimit = false; - } - } - - $q .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_sqlParts['where']) : ''; - $q .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; - $q .= ( ! empty($this->_sqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']): ''; - $q .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; - - if ($modifyLimit) { - $q = $this->_conn->modifyLimitQuery($q, $this->_sqlParts['limit'], $this->_sqlParts['offset']); - } - - // return to the previous state - if ( ! empty($string)) { - array_pop($this->_sqlParts['where']); - } - if ($needsSubQuery) { - array_shift($this->_sqlParts['where']); - } - $this->_sql = $q; - - return $q; - } /** - * getLimitSubquery - * this is method is used by the record limit algorithm + * This method is automatically called when this Doctrine_Hydrate is serialized. * - * when fetching one-to-many, many-to-many associated data with LIMIT clause - * an additional subquery is needed for limiting the number of returned records instead - * of limiting the number of sql result set rows - * - * @return string the limit subquery - * @todo A little refactor to make the method easier to understand & maybe shorter? - */ - public function getLimitSubquery() - { - $map = reset($this->_queryComponents); - $table = $map['table']; - $componentAlias = key($this->_queryComponents); - - // get short alias - $alias = $this->getTableAlias($componentAlias); - // what about composite keys? - $idFieldNames = (array)$table->getIdentifier(); - $primaryKey = $alias . '.' . $table->getColumnName($idFieldNames[0]); - - // initialize the base of the subquery - $subquery = 'SELECT DISTINCT ' . $this->_conn->quoteIdentifier($primaryKey); - - $driverName = $this->_conn->getAttribute(Doctrine::ATTR_DRIVER_NAME); - - // pgsql needs the order by fields to be preserved in select clause - if ($driverName == 'pgsql') { - foreach ($this->_sqlParts['orderby'] as $part) { - $part = trim($part); - $e = $this->_tokenizer->bracketExplode($part, ' '); - $part = trim($e[0]); - - if (strpos($part, '.') === false) { - continue; - } - - // don't add functions - if (strpos($part, '(') !== false) { - continue; - } - - // don't add primarykey column (its already in the select clause) - if ($part !== $primaryKey) { - $subquery .= ', ' . $part; - } - } - } - - if ($driverName == 'mysql' || $driverName == 'pgsql') { - foreach ($this->_expressionMap as $dqlAlias => $expr) { - if (isset($expr[1])) { - $subquery .= ', ' . $expr[0] . ' AS ' . $this->_aggregateAliasMap[$dqlAlias]; - } - } - } - - $subquery .= ' FROM'; - - foreach ($this->_sqlParts['from'] as $part) { - // preserve LEFT JOINs only if needed - if (substr($part, 0, 9) === 'LEFT JOIN') { - $e = explode(' ', $part); - - if (empty($this->_sqlParts['orderby']) && empty($this->_sqlParts['where'])) { - continue; - } - } - - $subquery .= ' ' . $part; - } - - // all conditions must be preserved in subquery - $subquery .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_sqlParts['where']) : ''; - $subquery .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; - $subquery .= ( ! empty($this->_sqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']) : ''; - - $subquery .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; - - // add driver specific limit clause - $subquery = $this->_conn->modifyLimitSubquery($table, $subquery, $this->_sqlParts['limit'], $this->_sqlParts['offset']); - - $parts = $this->_tokenizer->quoteExplode($subquery, ' ', "'", "'"); - - foreach ($parts as $k => $part) { - if (strpos($part, ' ') !== false) { - continue; - } - - $part = trim($part, "\"'`"); - - if ($this->hasSqlTableAlias($part)) { - $parts[$k] = $this->_conn->quoteIdentifier($this->generateNewSqlTableAlias($part)); - continue; - } - - if (strpos($part, '.') === false) { - continue; - } - preg_match_all("/[a-zA-Z0-9_]+\.[a-z0-9_]+/i", $part, $m); - - foreach ($m[0] as $match) { - $e = explode('.', $match); - $e[0] = $this->generateNewSqlTableAlias($e[0]); - - $parts[$k] = str_replace($match, implode('.', $e), $parts[$k]); - } - } - - if ($driverName == 'mysql' || $driverName == 'pgsql') { - foreach ($parts as $k => $part) { - if (strpos($part, "'") !== false) { - continue; - } - if (strpos($part, '__') == false) { - continue; - } - - preg_match_all("/[a-zA-Z0-9_]+\_\_[a-z0-9_]+/i", $part, $m); - - foreach ($m[0] as $match) { - $e = explode('__', $match); - $e[0] = $this->generateNewTableAlias($e[0]); - - $parts[$k] = str_replace($match, implode('__', $e), $parts[$k]); - } - } - } - - $subquery = implode(' ', $parts); - return $subquery; - } - - /** - * DQL PARSER - * parses a DQL query - * first splits the query in parts and then uses individual - * parsers for each part - * - * @param string $query DQL query - * @param boolean $clear whether or not to clear the aliases - * @throws Doctrine_Query_Exception if some generic parsing error occurs - * @return Doctrine_Query - */ - public function parseDqlQuery($query, $clear = true) - { - if ($clear) { - $this->clear(); - } - - $query = trim($query); - $query = str_replace("\n", ' ', $query); - $query = str_replace("\r", ' ', $query); - - $parts = $this->_tokenizer->tokenizeQuery($query); - - foreach ($parts as $partName => $subParts) { - $subParts = trim($subParts); - $partName = strtolower($partName); - switch ($partName) { - case 'create': - $this->_type = self::CREATE; - break; - case 'insert': - $this->_type = self::INSERT; - break; - case 'delete': - $this->_type = self::DELETE; - break; - case 'select': - $this->_type = self::SELECT; - $this->_addDqlQueryPart($partName, $subParts); - break; - case 'update': - $this->_type = self::UPDATE; - $partName = 'from'; - case 'from': - $this->_addDqlQueryPart($partName, $subParts); - break; - case 'set': - $this->_addDqlQueryPart($partName, $subParts, true); - break; - case 'group': - case 'order': - $partName .= 'by'; - case 'where': - case 'having': - case 'limit': - case 'offset': - $this->_addDqlQueryPart($partName, $subParts); - break; - } - } - - return $this; - } - - /** - * @todo Describe & refactor... too long and nested. - */ - public function load($path, $loadFields = true) - { - if (isset($this->_queryComponents[$path])) { - return $this->_queryComponents[$path]; - } - $e = $this->_tokenizer->quoteExplode($path, ' INDEXBY '); - - $mapWith = null; - if (count($e) > 1) { - $mapWith = trim($e[1]); - - $path = $e[0]; - } - - // parse custom join conditions - $e = explode(' ON ', $path); - - $joinCondition = ''; - - if (count($e) > 1) { - $joinCondition = $e[1]; - $overrideJoin = true; - $path = $e[0]; - } else { - $e = explode(' WITH ', $path); - - if (count($e) > 1) { - $joinCondition = $e[1]; - $path = $e[0]; - } - $overrideJoin = false; - } - - $tmp = explode(' ', $path); - $componentAlias = $originalAlias = (count($tmp) > 1) ? end($tmp) : null; - - $e = preg_split("/[.:]/", $tmp[0], -1); - - $fullPath = $tmp[0]; - $prevPath = ''; - $fullLength = strlen($fullPath); - - if (isset($this->_queryComponents[$e[0]])) { - $table = $this->_queryComponents[$e[0]]['table']; - $componentAlias = $e[0]; - - $prevPath = $parent = array_shift($e); - } - - foreach ($e as $key => $name) { - // get length of the previous path - $length = strlen($prevPath); - - // build the current component path - $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name; - - $delimeter = substr($fullPath, $length, 1); - - // if an alias is not given use the current path as an alias identifier - if (strlen($prevPath) === $fullLength && isset($originalAlias)) { - $componentAlias = $originalAlias; - } else { - $componentAlias = $prevPath; - } - - // if the current alias already exists, it's user error - if (isset($this->_queryComponents[$componentAlias])) { - throw new Doctrine_Query_Exception("Duplicate alias '$componentAlias' in query."); - } - - if ( ! isset($table)) { - // process the root of the path - $table = $this->loadRoot($name, $componentAlias); - } else { - $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN '; - $relation = $table->getRelation($name); - $localTable = $table; - - $table = $relation->getTable(); - //echo "

" . $table->getComponentName() . "------" . $relation->getForeignComponentName() . "

"; - $this->_queryComponents[$componentAlias] = array( - 'table' => $table, - 'mapper' => $this->_conn->getMapper($relation->getForeignComponentName()), - 'parent' => $parent, - 'relation' => $relation, - 'map' => null - ); - if ( ! $relation->isOneToOne()) { - $this->_needsSubquery = true; - } - - $localAlias = $this->getTableAlias($parent, $table->getTableName()); - $foreignAlias = $this->getTableAlias($componentAlias, $relation->getTable()->getTableName()); - $localSql = $this->_conn->quoteIdentifier($table->getTableName()) - . ' ' - . $this->_conn->quoteIdentifier($localAlias); - - $foreignSql = $this->_conn->quoteIdentifier($relation->getTable()->getTableName()) - . ' ' - . $this->_conn->quoteIdentifier($foreignAlias); - - $map = $relation->getTable()->getOption('inheritanceMap'); - - if ( ! $loadFields || ! empty($map) || $joinCondition) { - $this->_subqueryAliases[] = $foreignAlias; - } - - if ($relation instanceof Doctrine_Relation_Association) { - $asf = $relation->getAssociationTable(); - - $assocTableName = $asf->getTableName(); - - if ( ! $loadFields || ! empty($map) || $joinCondition) { - $this->_subqueryAliases[] = $assocTableName; - } - - $assocPath = $prevPath . '.' . $asf->getComponentName(); - //echo "

" . $asf->getComponentName() . "---2---" . $relation->getForeignComponentName() . "

"; - $this->_queryComponents[$assocPath] = array( - 'parent' => $prevPath, - 'relation' => $relation, - 'table' => $asf, - 'mapper' => $this->_conn->getMapper($relation->getAssociationClassName()) - ); - - $assocAlias = $this->getTableAlias($assocPath, $asf->getTableName()); - - $queryPart = $join . $assocTableName . ' ' . $assocAlias; - - $localTableIdFieldNames = (array)$localTable->getIdentifier(); - $queryPart .= ' ON ' . $localAlias - . '.' - . $localTable->getColumnName($localTableIdFieldNames[0]) // what about composite keys? - . ' = ' - . $assocAlias . '.' . $relation->getLocal(); - - $tableIdFieldNames = (array)$table->getIdentifier(); - if ($relation->isEqual()) { - // equal nest relation needs additional condition - $queryPart .= ' OR ' . $localAlias - . '.' - . $table->getColumnName($tableIdFieldNames[0]) - . ' = ' - . $assocAlias . '.' . $relation->getForeign(); - } - - $this->_sqlParts['from'][] = $queryPart; - - $queryPart = $join . $foreignSql; - - if ( ! $overrideJoin) { - $queryPart .= ' ON '; - - if ($relation->isEqual()) { - $queryPart .= '('; - } - - $relationTable = $relation->getTable(); - $relationTableIdFieldNames = (array)$relationTable->getIdentifier(); - $queryPart .= $this->_conn->quoteIdentifier($foreignAlias . '.' . $relationTable->getColumnName($relationTableIdFieldNames[0])) - . ' = ' - . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeign()); - - if ($relation->isEqual()) { - $queryPart .= ' OR ' - . $this->_conn->quoteIdentifier($foreignAlias . '.' . $table->getColumnName($tableIdFieldNames[0])) - . ' = ' - . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocal()) - . ') AND ' - . $this->_conn->quoteIdentifier($foreignAlias . '.' . $table->getColumnName($tableIdFieldNames[0])) - . ' != ' - . $this->_conn->quoteIdentifier($localAlias . '.' . $table->getColumnName($tableIdFieldNames[0])); - } - } - } else { - $queryPart = $join . $foreignSql; - - if ( ! $overrideJoin) { - $queryPart .= ' ON ' - . $this->_conn->quoteIdentifier($localAlias . '.' . $relation->getLocal()) - . ' = ' - . $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getForeign()); - } - } - - $queryPart .= $this->_createCustomJoinSql($table->getComponentName(), $componentAlias); - - $this->_sqlParts['from'][$componentAlias] = $queryPart; - if ( ! empty($joinCondition)) { - $this->_pendingJoinConditions[$componentAlias] = $joinCondition; - } - } - - if ($loadFields) { - $restoreState = false; - // load fields if necessary - if ($loadFields && empty($this->_dqlParts['select'])) { - $this->_pendingFields[$componentAlias] = array('*'); - } - } - $parent = $prevPath; - } - - $table = $this->_queryComponents[$componentAlias]['table']; - $mapper = $this->_queryComponents[$componentAlias]['mapper']; - - $indexBy = null; - - if (isset($mapWith)) { - $e = explode('.', $mapWith); - - if (isset($e[1])) { - $indexBy = $e[1]; - } - } else if ($table->getBoundQueryPart('indexBy') !== null) { - $indexBy = $table->getBoundQueryPart('indexBy'); - } - - if ($indexBy !== null) { - if ( ! $table->hasField($indexBy)) { - throw new Doctrine_Query_Exception("Couldn't use key mapping. Column " . $indexBy . " does not exist."); - } - - $this->_queryComponents[$componentAlias]['map'] = $indexBy; - } - return $this->_queryComponents[$componentAlias]; - } - - /** - * loadRoot - * - * @param string $name - * @param string $componentAlias - * @todo DESCRIBE ME! - */ - public function loadRoot($name, $componentAlias) - { - // get the connection for the component - $manager = Doctrine_Manager::getInstance(); - if ($manager->hasConnectionForComponent($name)) { - $this->_conn = $manager->getConnectionForComponent($name); - } - - $table = $this->_conn->getMetadata($name); - $tableName = $table->getTableName(); - - // get the short alias for this table - $tableAlias = $this->getTableAlias($componentAlias, $tableName); - // quote table name - $queryPart = $this->_conn->quoteIdentifier($tableName); - - if ($this->_type === self::SELECT) { - $queryPart .= ' ' . $this->_conn->quoteIdentifier($tableAlias); - } - - $this->_tableAliasMap[$tableAlias] = $componentAlias; - - $queryPart .= $this->_createCustomJoinSql($name, $componentAlias); - - $this->_sqlParts['from'][] = $queryPart; - //echo "

" . $table->getComponentName() . "---3---" . $name . "

"; - $this->_queryComponents[$componentAlias] = array( - 'table' => $table, 'mapper' => $this->_conn->getEntityPersister($name), 'map' => null); - - return $table; - } - - /** - * count - * fetches the count of the query - * - * This method executes the main query without all the - * selected fields, ORDER BY part, LIMIT part and OFFSET part. - * - * Example: - * Main query: - * SELECT u.*, p.phonenumber FROM User u - * LEFT JOIN u.Phonenumber p - * WHERE p.phonenumber = '123 123' LIMIT 10 - * - * The modified DQL query: - * SELECT COUNT(DISTINCT u.id) FROM User u - * LEFT JOIN u.Phonenumber p - * WHERE p.phonenumber = '123 123' - * - * @param array $params an array of prepared statement parameters - * @return integer the count of this query - */ - public function count($params = array()) - { - // triggers dql parsing/processing - $this->getQuery(); // this is ugly - - // initialize temporary variables - $where = $this->_sqlParts['where']; - $having = $this->_sqlParts['having']; - $groupby = $this->_sqlParts['groupby']; - $map = reset($this->_queryComponents); - $componentAlias = key($this->_queryComponents); - $table = $map['table']; - - // build the query base - $q = 'SELECT COUNT(DISTINCT ' . $this->getTableAlias($componentAlias) - . '.' . implode(',', $table->getIdentifierColumnNames()) - . ') AS num_results'; - - foreach ($this->_sqlParts['select'] as $field) { - if (strpos($field, '(') !== false) { - $q .= ', ' . $field; - } - } - - $q .= ' FROM ' . $this->_buildSqlFromPart(); - - // append discriminator column conditions (if any) - $string = $this->_createDiscriminatorConditionSql(); - if ( ! empty($string)) { - $where[] = $string; - } - - // append conditions - $q .= ( ! empty($where)) ? ' WHERE ' . implode(' AND ', $where) : ''; - $q .= ( ! empty($groupby)) ? ' GROUP BY ' . implode(', ', $groupby) : ''; - $q .= ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having): ''; - - if ( ! is_array($params)) { - $params = array($params); - } - // append parameters - $params = array_merge($this->_params['where'], $this->_params['having'], $params); - - $params = $this->convertEnums($params); - - $results = $this->getConnection()->fetchAll($q, $params); - - if (count($results) > 1) { - $count = 0; - foreach ($results as $result) { - $count += $result['num_results']; - } - } else { - $count = isset($results[0]) ? $results[0]['num_results']:0; - } - - return (int) $count; - } - - /** - * query - * query the database with DQL (Doctrine Query Language) - * - * @param string $query DQL query - * @param array $params prepared statement parameters - * @param int $hydrationMode Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD - * @see Doctrine::FETCH_* constants - * @return mixed - */ - public function query($query, $params = array(), $hydrationMode = null) - { - $this->parseDqlQuery($query); - return $this->execute($params, $hydrationMode); - } - - /** - * Copies a Doctrine_Query object. - * - * @param Doctrine_Query Doctrine query instance. - * If ommited the instance itself will be used as source. - * @return Doctrine_Query Copy of the Doctrine_Query instance. - */ - public function copy(Doctrine_Query $query = null) - { - if ( ! $query) { - $query = $this; - } - - $new = new Doctrine_Query(); - $new->_dqlParts = $query->_dqlParts; - $new->_params = $query->_params; - $new->_hydrator = $query->_hydrator; - - return $new; - } - - /** - * Frees the resources used by the query object. It especially breaks a - * cyclic reference between the query object and it's parsers. This enables - * PHP's current GC to reclaim the memory. - * This method can therefore be used to reduce memory usage when creating a lot - * of query objects during a request. - * - * @return Doctrine_Query this object - */ - public function free() - { - $this->reset(); - $this->_parsers = array(); - $this->_dqlParts = array(); - $this->_enumParams = array(); - } - - /** - * serialize - * this method is automatically called when this Doctrine_Hydrate is serialized - * - * @return array an array of serialized properties + * @return array An array of serialized properties */ public function serialize() { @@ -1793,10 +598,9 @@ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable, Seria } /** - * unseralize - * this method is automatically called everytime a Doctrine_Hydrate object is unserialized + * This method is automatically called everytime a Doctrine_Hydrate object is unserialized. * - * @param string $serialized Doctrine_Entity as serialized string + * @param string $serialized Doctrine_Record as serialized string * @return void */ public function unserialize($serialized) diff --git a/lib/Doctrine/Query/Abstract.php b/lib/Doctrine/Query/Abstract.php old mode 100644 new mode 100755 index 059229ccc..3e0d327a9 --- a/lib/Doctrine/Query/Abstract.php +++ b/lib/Doctrine/Query/Abstract.php @@ -1,6 +1,7 @@ * @author Konsta Vesterinen - * @todo See {@link Doctrine_Query} + * @todo See {@link Doctrine_Query} */ abstract class Doctrine_Query_Abstract { @@ -38,1268 +40,380 @@ abstract class Doctrine_Query_Abstract */ /** - * constant for SELECT queries + * Constant for SELECT queries. */ const SELECT = 0; /** - * constant for DELETE queries + * Constant for DELETE queries. */ const DELETE = 1; /** - * constant for UPDATE queries + * Constant for UPDATE queries. */ const UPDATE = 2; /** - * constant for INSERT queries + * @todo [TODO] Remove these ones (INSERT and CREATE)? */ - const INSERT = 3; /** - * constant for CREATE queries + * Constant for INSERT queries. */ - const CREATE = 4; - - /** @todo document the query states (and the transitions between them). */ + //const INSERT = 3; + + /** + * Constant for CREATE queries. + */ + //const CREATE = 4; + + /** * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts. */ const STATE_CLEAN = 1; - + /** - * A query object is in state DIRTY when it has DQL parts that have not yet been - * parsed/processed. + * A query object is in state DIRTY when it has DQL parts that have not yet been + * parsed/processed. This is automatically defined as DIRTY when addDqlQueryPart + * is called. */ const STATE_DIRTY = 2; - + + /** + * @todo [TODO] Remove these ones (DIRECT and LOCKED)? + */ + /** * A query is in DIRECT state when ... ? */ - const STATE_DIRECT = 3; - + //const STATE_DIRECT = 3; + /** * A query object is on LOCKED state when ... ? */ - const STATE_LOCKED = 4; - + //const STATE_LOCKED = 4; + + /** - * @var array Table alias map. Keys are SQL aliases and values DQL aliases. + * @var integer $type Query type. + * + * @see Doctrine_Query::* constants */ - protected $_tableAliasMap = array(); - - /** - * @var Doctrine_View The view object used by this query, if any. - */ - protected $_view; - + protected $_type = self::SELECT; + /** * @var integer $_state The current state of this query. */ - protected $_state = Doctrine_Query::STATE_CLEAN; + protected $_state = self::STATE_CLEAN; /** - * @var array $params The parameters of this query. + * @var array $params Parameters of this query. + * @see Doctrine_Query::free that initializes this property */ - protected $_params = array('join' => array(), - 'where' => array(), - 'set' => array(), - 'having' => array()); - - /* Caching properties */ - /** - * @var Doctrine_Cache_Interface The cache driver used for caching result sets. - */ - protected $_resultCache; + protected $_params = array(); + /** - * @var boolean $_expireResultCache A boolean value that indicates whether or not - * expire the result cache. + * @var array $_enumParams Array containing the keys of the parameters that should be enumerated. + * @see Doctrine_Query::free that initializes this property */ - protected $_expireResultCache = false; - protected $_resultCacheTTL; - - /** - * @var Doctrine_Cache_Interface The cache driver used for caching queries. - */ - protected $_queryCache; - protected $_expireQueryCache = false; - protected $_queryCacheTTL; - - + protected $_enumParams = array(); + /** - * @var Doctrine_Connection The connection used by this query object. + * @var array $_dqlParts An array containing all DQL query parts. + * @see Doctrine_Query::free that initializes this property */ - protected $_conn; - - + protected $_dqlParts = array(); + /** - * @var array $_sqlParts The SQL query string parts. Filled during the DQL parsing process. + * @var string $_dql Cached DQL query. */ - protected $_sqlParts = array( + protected $_dql = null; + + + /** + * Frees the resources used by the query object. It especially breaks a + * cyclic reference between the query object and it's parsers. This enables + * PHP's current GC to reclaim the memory. + * This method can therefore be used to reduce memory usage when creating a lot + * of query objects during a request. + */ + public function free() + { + /** + * @todo [TODO] What about "forUpdate" support? Remove it? + */ + $this->_dqlParts = array( 'select' => array(), 'distinct' => false, 'forUpdate' => false, 'from' => array(), - 'set' => array(), 'join' => array(), - 'where' => array(), - 'groupby' => array(), - 'having' => array(), - 'orderby' => array(), - 'limit' => false, - 'offset' => false, - ); - - /** - * @var array $_dqlParts An array containing all DQL query parts. - */ - protected $_dqlParts = array( - 'from' => array(), - 'select' => array(), - 'forUpdate' => false, 'set' => array(), - 'join' => array(), 'where' => array(), 'groupby' => array(), 'having' => array(), 'orderby' => array(), 'limit' => array(), 'offset' => array(), - ); - - + ); + + $this->_params = array( + 'join' => array(), + 'set' => array(), + 'where' => array(), + 'having' => array() + ); + + $this->_enumParams = array(); + + $this->_dql = null; + $this->_state = self::STATE_CLEAN; + } + + /** - * @var array $_queryComponents Two dimensional array containing the components of this query, - * informations about their relations and other related information. - * The components are constructed during query parsing. + * Defines a complete DQL * - * Keys are component aliases and values the following: - * - * table table object associated with given alias - * - * relation the relation object owned by the parent - * - * parent the alias of the parent - * - * agg the aggregates of this component - * - * map the name of the column / aggregate value this - * component is mapped to a collection + * @param string $dqlQuery DQL Query */ - protected $_queryComponents = array(); - - /** - * @var integer $type the query type - * - * @see Doctrine_Query::* constants - */ - protected $_type = self::SELECT; - - /** - * @var Doctrine_Hydrator The hydrator object used to hydrate query results. - */ - protected $_hydrator; - - /** - * @var Doctrine_Query_Tokenizer The tokenizer that is used during the query parsing process. - */ - protected $_tokenizer; - - /** - * @var Doctrine_Query_Parser The parser that is used for query parsing. - */ - protected $_parser; - - /** - * @var array $_tableAliasSeeds A simple array keys representing table aliases and values - * table alias seeds. The seeds are used for generating short table - * aliases. - */ - protected $_tableAliasSeeds = array(); - - /** - * @var array $_options an array of options - */ - protected $_options = array( - 'fetchMode' => Doctrine::FETCH_RECORD - ); - - /** - * @var array $_enumParams an array containing the keys of the parameters that should be enumerated - */ - protected $_enumParams = array(); - - /** - * @var boolean - */ - protected $_isLimitSubqueryUsed = false; - - - /** - * Constructor. - * - * @param Doctrine_Connection The connection object the query will use. - * @param Doctrine_Hydrator_Abstract The hydrator that will be used for generating result sets. - */ - public function __construct(Doctrine_Connection $connection = null, - Doctrine_Hydrator_Abstract $hydrator = null) + public function setDql($dqlQuery) { - if ($connection === null) { - $connection = Doctrine_Manager::getInstance()->getCurrentConnection(); + $this->free(); + + if ($dqlQuery !== null) { + $this->_dql = $dqlQuery; + + $this->_state = self::STATE_DIRTY; } - if ($hydrator === null) { - $hydrator = new Doctrine_Hydrator($connection); - } - $this->_conn = $connection; - $this->_hydrator = $hydrator; - $this->_tokenizer = new Doctrine_Query_Tokenizer(); } - + + /** - * setOption + * Returns the DQL query that is represented by this query object. * - * @param string $name option name - * @param string $value option value - * @return Doctrine_Query this object + * @return string DQL query */ - public function setOption($name, $value) + public function getDql() { - if ( ! isset($this->_options[$name])) { - throw new Doctrine_Query_Exception('Unknown option ' . $name); - } - $this->_options[$name] = $value; - } - - /** - * hasTableAlias - * whether or not this object has given tableAlias - * - * @param string $tableAlias the table alias to be checked - * @return boolean true if this object has given alias, otherwise false - * @deprecated - */ - public function hasTableAlias($sqlTableAlias) - { - return $this->hasSqlTableAlias($sqlTableAlias); - } - - /** - * hasSqlTableAlias - * whether or not this object has given tableAlias - * - * @param string $tableAlias the table alias to be checked - * @return boolean true if this object has given alias, otherwise false - */ - public function hasSqlTableAlias($sqlTableAlias) - { - return (isset($this->_tableAliasMap[$sqlTableAlias])); - } - - /** - * getTableAliases - * returns all table aliases - * - * @return array table aliases as an array - * @deprecated - */ - public function getTableAliases() - { - return $this->getTableAliasMap(); - } - - /** - * getTableAliasMap - * returns all table aliases - * - * @return array table aliases as an array - */ - public function getTableAliasMap() - { - return $this->_tableAliasMap; - } - - /** - * getQueryPart - * gets a query part from the query part array - * - * @param string $name the name of the query part to be set - * @param string $part query part string - * @throws Doctrine_Query_Exception if trying to set unknown query part - * @return Doctrine_Query_Abstract this object - * @deprecated - */ - public function getQueryPart($part) - { - return $this->getSqlQueryPart($part); - } - - /** - * getSqlQueryPart - * gets an SQL query part from the SQL query part array - * - * @param string $name the name of the query part to be set - * @param string $part query part string - * @throws Doctrine_Query_Exception if trying to set unknown query part - * @return Doctrine_Hydrate this object - */ - public function getSqlQueryPart($part) - { - if ( ! isset($this->_sqlParts[$part])) { - throw new Doctrine_Query_Exception('Unknown SQL query part ' . $part); - } - return $this->_sqlParts[$part]; - } - - /** - * setQueryPart - * sets a query part in the query part array - * - * @param string $name the name of the query part to be set - * @param string $part query part string - * @throws Doctrine_Query_Exception if trying to set unknown query part - * @return Doctrine_Hydrate this object - * @deprecated - */ - public function setQueryPart($name, $part) - { - return $this->setSqlQueryPart($name, $part); - } - - /** - * setSqlQueryPart - * sets an SQL query part in the SQL query part array - * - * @param string $name the name of the query part to be set - * @param string $part query part string - * @throws Doctrine_Query_Exception if trying to set unknown query part - * @return Doctrine_Hydrate this object - */ - public function setSqlQueryPart($name, $part) - { - if ( ! isset($this->_sqlParts[$name])) { - throw new Doctrine_Query_Exception('Unknown query part ' . $name); + if ($this->_dql !== null) { + return $this->_dql; } - if ($name !== 'limit' && $name !== 'offset') { - if (is_array($part)) { - $this->_sqlParts[$name] = $part; - } else { - $this->_sqlParts[$name] = array($part); - } - } else { - $this->_sqlParts[$name] = $part; + $dql = ''; + + switch ($this->_type) { + case self::DELETE: + $dql = $this->_getDqlForDelete(); + break; + + case self::UPDATE: + $dql = $this->_getDqlForUpdate(); + break; + + /** + * @todo [TODO] Remove these ones (INSERT and CREATE)? + */ + /* + case self::INSERT: + break; + + case self::CREATE: + break; + */ + + case self::SELECT: + default: + $dql = $this->_getDqlForSelect(); + break; } - return $this; + return $dql; } - - /** - * addQueryPart - * adds a query part in the query part array - * - * @param string $name the name of the query part to be added - * @param string $part query part string - * @throws Doctrine_Query_Exception if trying to add unknown query part - * @return Doctrine_Hydrate this object - * @deprecated - */ - public function addQueryPart($name, $part) - { - return $this->addSqlQueryPart($name, $part); - } - - /** - * addSqlQueryPart - * adds an SQL query part to the SQL query part array - * - * @param string $name the name of the query part to be added - * @param string $part query part string - * @throws Doctrine_Query_Exception if trying to add unknown query part - * @return Doctrine_Hydrate this object - */ - public function addSqlQueryPart($name, $part) - { - if ( ! isset($this->_sqlParts[$name])) { - throw new Doctrine_Query_Exception('Unknown query part ' . $name); - } - if (is_array($part)) { - $this->_sqlParts[$name] = array_merge($this->_sqlParts[$name], $part); - } else { - $this->_sqlParts[$name][] = $part; - } - return $this; - } - - /** - * removeQueryPart - * removes a query part from the query part array - * - * @param string $name the name of the query part to be removed - * @throws Doctrine_Query_Exception if trying to remove unknown query part - * @return Doctrine_Hydrate this object - * @deprecated - */ - public function removeQueryPart($name) - { - return $this->removeSqlQueryPart($name); - } - - /** - * removeSqlQueryPart - * removes a query part from the query part array - * - * @param string $name the name of the query part to be removed - * @throws Doctrine_Query_Exception if trying to remove unknown query part - * @return Doctrine_Hydrate this object - */ - public function removeSqlQueryPart($name) - { - try { - if ( ! isset($this->_sqlParts[$name])) { - throw new Doctrine_Query_Exception('Unknown query part ' . $name); - }} - catch (Exception $e) {echo $e->getTraceAsString(); echo "


";} - if ($name == 'limit' || $name == 'offset') { - $this->_sqlParts[$name] = false; - } else { - $this->_sqlParts[$name] = array(); - } - return $this; - } - - /** - * setView - * sets a database view this query object uses - * this method should only be called internally by doctrine - * - * @param Doctrine_View $view database view - * @return void - */ - public function setView(Doctrine_View $view) - { - $this->_view = $view; - } - - /** - * getView - * returns the view associated with this query object (if any) - * - * @return Doctrine_View the view associated with this query object - */ - public function getView() - { - return $this->_view; - } - - /** - * limitSubqueryUsed - * - * @return boolean - */ - public function isLimitSubqueryUsed() - { - return $this->_isLimitSubqueryUsed; - } - - /** - * convertEnums - * convert enum parameters to their integer equivalents - * - * @return array converted parameter array - */ - public function convertEnums($params) - { - foreach ($this->_enumParams as $key => $values) { - if (isset($params[$key])) { - if ( ! empty($values)) { - $params[$key] = $values[0]->enumIndex($values[1], $params[$key]); - } - } - } - return $params; - } - - /** - * Creates the SQL snippet for additional joins. - * - * @return string The created SQL snippet. - */ - protected function _createCustomJoinSql($componentName, $componentAlias) - { - $table = $this->_conn->getMetadata($componentName); - $tableAlias = $this->getSqlTableAlias($componentAlias, $table->getTableName()); - $customJoins = $this->_conn->getEntityPersister($componentName)->getCustomJoins(); - $sql = ''; - foreach ($customJoins as $componentName => $joinType) { - $joinedTable = $this->_conn->getMetadata($componentName); - $joinedAlias = $componentAlias . '.' . $componentName; - $joinedTableAlias = $this->getSqlTableAlias($joinedAlias, $joinedTable->getTableName()); - $sql .= " $joinType JOIN " . $this->_conn->quoteIdentifier($joinedTable->getTableName()) - . ' ' . $this->_conn->quoteIdentifier($joinedTableAlias) . ' ON '; - - foreach ($table->getIdentifierColumnNames() as $column) { - $sql .= $this->_conn->quoteIdentifier($tableAlias) - . '.' . $this->_conn->quoteIdentifier($column) - . ' = ' . $this->_conn->quoteIdentifier($joinedTableAlias) - . '.' . $this->_conn->quoteIdentifier($column); - } - } - - return $sql; - } - - /** - * Creates the SQL snippet for the WHERE part that contains the discriminator - * column conditions. - * Used solely for Single Table Inheritance. - * - * @return string The created SQL snippet. - */ - protected function _createDiscriminatorConditionSql() - { - $array = array(); - foreach ($this->_queryComponents as $componentAlias => $data) { - $sqlTableAlias = $this->getSqlTableAlias($componentAlias); - if ($data['table']->getInheritanceType() != Doctrine::INHERITANCE_TYPE_SINGLE_TABLE) { - $array[$sqlTableAlias][] = array(); - } else { - $discCol = $data['table']->getInheritanceOption('discriminatorColumn'); - $discMap = $data['table']->getInheritanceOption('discriminatorMap'); - $discValue = array_search($data['table']->getClassName(), $discMap); - if ($discValue === false) { - continue; - } - $discriminator = array(); - $discriminator[] = array($discCol => $discValue); - - $subclasses = $data['table']->getSubclasses(); - foreach ((array)$subclasses as $subclass) { - $subClassMetadata = $this->_conn->getClassMetadata($subclass); - $discCol = $subClassMetadata->getInheritanceOption('discriminatorColumn'); - $discMap = $subClassMetadata->getInheritanceOption('discriminatorMap'); - $discValue = array_search($subclass, $discMap); - $discriminator[] = array($discCol => $discValue); - } - - $array[$sqlTableAlias][] = $discriminator; - } - } - //var_dump($array); - // apply inheritance maps - $str = ''; - $c = array(); - $index = 0; - foreach ($array as $tableAlias => $maps) { - $a = array(); + /** + * Builds the DQL of DELETE + */ + protected function _getDqlForDelete() + { + /* + * BNF: + * + * DeleteStatement = DeleteClause [WhereClause] [OrderByClause] [LimitClause] [OffsetClause] + * DeleteClause = "DELETE" "FROM" RangeVariableDeclaration + * WhereClause = "WHERE" ConditionalExpression + * OrderByClause = "ORDER" "BY" OrderByItem {"," OrderByItem} + * LimitClause = "LIMIT" integer + * OffsetClause = "OFFSET" integer + * + */ + return 'DELETE' + . $this->_getReducedDqlQueryPart('from', array('pre' => ' FROM ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('where', array('pre' => ' WHERE ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('orderby', array('pre' => ' ORDER BY ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('limit', array('pre' => ' LIMIT ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('offset', array('pre' => ' OFFSET ', 'separator' => ' ')); + } - // don't use table aliases if the query isn't a select query - if ($this->_type !== Doctrine_Query::SELECT) { - $tableAlias = ''; - } else { - $tableAlias .= '.'; - } - foreach ($maps as $map) { - //echo "start"; - $b = array(); - foreach ($map as $discriminator) { - list($column, $value) = each($discriminator); - $identifier = $this->_conn->quoteIdentifier($tableAlias . $column); - if ($index > 0) { - $b[] = '(' . $identifier . ' = ' . $this->_conn->quote($value) - . ' OR ' . $identifier . ' IS NULL)'; - } else { - $b[] = $identifier . ' = ' . $this->_conn->quote($value); - } - } + /** + * Builds the DQL of UPDATE + */ + protected function _getDqlForUpdate() + { + /* + * BNF: + * + * UpdateStatement = UpdateClause [WhereClause] [OrderByClause] [LimitClause] [OffsetClause] + * UpdateClause = "UPDATE" RangeVariableDeclaration "SET" UpdateItem {"," UpdateItem} + * WhereClause = "WHERE" ConditionalExpression + * OrderByClause = "ORDER" "BY" OrderByItem {"," OrderByItem} + * LimitClause = "LIMIT" integer + * OffsetClause = "OFFSET" integer + * + */ + return 'UPDATE' + . $this->_getReducedDqlQueryPart('from', array('pre' => ' FROM ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('where', array('pre' => ' SET ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('where', array('pre' => ' WHERE ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('orderby', array('pre' => ' ORDER BY ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('limit', array('pre' => ' LIMIT ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('offset', array('pre' => ' OFFSET ', 'separator' => ' ')); + } - if ( ! empty($b)) { - if (count($b) > 1) { - $a[] = '(' . implode(' OR ', $b) . ')'; - } else { - $a[] = implode(' OR ', $b); - } - } - } - //echo "end
"; - if ( ! empty($a)) { - $c[] = implode(' AND ', $a); - } - $index++; + + /** + * Builds the DQL of SELECT + */ + protected function _getDqlForSelect() + { + /* + * BNF: + * + * SelectStatement = [SelectClause] FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] [LimitClause] [OffsetClause] + * SelectClause = "SELECT" ["ALL" | "DISTINCT"] SelectExpression {"," SelectExpression} + * FromClause = "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration} + * WhereClause = "WHERE" ConditionalExpression + * GroupByClause = "GROUP" "BY" GroupByItem {"," GroupByItem} + * HavingClause = "HAVING" ConditionalExpression + * OrderByClause = "ORDER" "BY" OrderByItem {"," OrderByItem} + * LimitClause = "LIMIT" integer + * OffsetClause = "OFFSET" integer + * + */ + /** + * @todo [TODO] What about "ALL" support? + */ + return 'SELECT' + . (($this->getDqlQueryPart('distinct') === true) ? ' DISTINCT' : '') + . $this->_getReducedDqlQueryPart('select', array('pre' => ' ', 'separator' => ', ', 'empty' => ' *')) + . $this->_getReducedDqlQueryPart('from', array('pre' => ' FROM ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('where', array('pre' => ' WHERE ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('groupby', array('pre' => ' GROUP BY ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('having', array('pre' => ' HAVING ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('orderby', array('pre' => ' ORDER BY ', 'separator' => ', ')) + . $this->_getReducedDqlQueryPart('limit', array('pre' => ' LIMIT ', 'separator' => ' ')) + . $this->_getReducedDqlQueryPart('offset', array('pre' => ' OFFSET ', 'separator' => ' ')); + } + + + /** + * @nodoc + */ + protected function _getReducedDqlQueryPart($queryPartName, $options = array()) + { + if (empty($this->_dqlParts[$queryPartName])) { + return (isset($options['empty']) ? $options['empty'] : ''); } - $str .= implode(' AND ', $c); + $str = (isset($options['pre']) ? $options['pre'] : ''); + $str .= implode($options['separator'], $this->getDqlQueryPart($queryPartName)); + $str .= (isset($options['post']) ? $options['post'] : ''); return $str; } - + /** - * getTableAlias - * some database such as Oracle need the identifier lengths to be < ~30 chars - * hence Doctrine creates as short identifier aliases as possible + * Returns the type of this query object + * By default the type is Doctrine_Query_Abstract::SELECT but if update() or delete() + * are being called the type is Doctrine_Query_Abstract::UPDATE and Doctrine_Query_Abstract::DELETE, + * respectively. * - * this method is used for the creation of short table aliases, its also - * smart enough to check if an alias already exists for given component (componentAlias) + * @see Doctrine_Query_Abstract::SELECT + * @see Doctrine_Query_Abstract::UPDATE + * @see Doctrine_Query_Abstract::DELETE * - * @param string $componentAlias the alias for the query component to search table alias for - * @param string $tableName the table name from which the table alias is being created - * @return string the generated / fetched short alias - * @deprecated + * @return integer Return the query type */ - public function getTableAlias($componentAlias, $tableName = null) + public function getType() { - return $this->getSqlTableAlias($componentAlias, $tableName); - } - - /** - * getSqlTableAlias - * some database such as Oracle need the identifier lengths to be < ~30 chars - * hence Doctrine creates as short identifier aliases as possible - * - * this method is used for the creation of short table aliases, its also - * smart enough to check if an alias already exists for given component (componentAlias) - * - * @param string $componentAlias the alias for the query component to search table alias for - * @param string $tableName the table name from which the table alias is being created - * @return string the generated / fetched short alias - */ - public function getSqlTableAlias($componentAlias, $tableName = null) - { - $alias = array_search($componentAlias, $this->_tableAliasMap); - - if ($alias !== false) { - return $alias; - } - - if ($tableName === null) { - throw new Doctrine_Query_Exception("Couldn't get short alias for " . $componentAlias); - } - - return $this->generateTableAlias($componentAlias, $tableName); - } - - /** - * generateNewTableAlias - * generates a new alias from given table alias - * - * @param string $tableAlias table alias from which to generate the new alias from - * @return string the created table alias - * @deprecated - */ - public function generateNewTableAlias($oldAlias) - { - return $this->generateNewSqlTableAlias($oldAlias); - } - - /** - * generateNewSqlTableAlias - * generates a new alias from given table alias - * - * @param string $tableAlias table alias from which to generate the new alias from - * @return string the created table alias - */ - public function generateNewSqlTableAlias($oldAlias) - { - if (isset($this->_tableAliasMap[$oldAlias])) { - // generate a new alias - $name = substr($oldAlias, 0, 1); - $i = ((int) substr($oldAlias, 1)); - - if ($i == 0) { - $i = 1; - } - - $newIndex = ($this->_tableAliasSeeds[$name] + $i); - - return $name . $newIndex; - } - - return $oldAlias; - } - - /** - * getTableAliasSeed - * returns the alias seed for given table alias - * - * @param string $tableAlias table alias that identifies the alias seed - * @return integer table alias seed - * @deprecated - */ - public function getTableAliasSeed($sqlTableAlias) - { - return $this->getSqlTableAliasSeed($sqlTableAlias); - } - - /** - * getSqlTableAliasSeed - * returns the alias seed for given table alias - * - * @param string $tableAlias table alias that identifies the alias seed - * @return integer table alias seed - */ - public function getSqlTableAliasSeed($sqlTableAlias) - { - if ( ! isset($this->_tableAliasSeeds[$sqlTableAlias])) { - return 0; - } - return $this->_tableAliasSeeds[$sqlTableAlias]; - } - - /** - * hasAliasDeclaration - * whether or not this object has a declaration for given component alias - * - * @param string $componentAlias the component alias the retrieve the declaration from - * @return boolean - */ - public function hasAliasDeclaration($componentAlias) - { - return isset($this->_queryComponents[$componentAlias]); - } - - /** - * getAliasDeclaration - * get the declaration for given component alias - * - * @param string $componentAlias the component alias the retrieve the declaration from - * @return array the alias declaration - * @deprecated - */ - public function getAliasDeclaration($componentAlias) - { - return $this->getQueryComponent($componentAlias); - } - - /** - * getQueryComponent - * get the declaration for given component alias - * - * @param string $componentAlias the component alias the retrieve the declaration from - * @return array the alias declaration - */ - public function getQueryComponent($componentAlias) - { - if ( ! isset($this->_queryComponents[$componentAlias])) { - throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); - } - - return $this->_queryComponents[$componentAlias]; - } - - /** - * copyAliases - * copy aliases from another Hydrate object - * - * this method is needed by DQL subqueries which need the aliases - * of the parent query - * - * @param Doctrine_Hydrate $query the query object from which the - * aliases are copied from - * @return Doctrine_Hydrate this object - */ - public function copyAliases(Doctrine_Query_Abstract $query) - { - $this->_tableAliasMap = $query->_tableAliasMap; - $this->_queryComponents = $query->_queryComponents; - $this->_tableAliasSeeds = $query->_tableAliasSeeds; - return $this; - } - - /** - * getRootAlias - * returns the alias of the the root component - * - * @return array - */ - public function getRootAlias() - { - if ( ! $this->_queryComponents) { - $this->getSql(); - } - reset($this->_queryComponents); - - return key($this->_queryComponents); - } - - /** - * getRootDeclaration - * returns the root declaration - * - * @return array - */ - public function getRootDeclaration() - { - $map = reset($this->_queryComponents); - return $map; - } - - /** - * getRoot - * returns the root component for this object - * - * @return Doctrine_Table root components table - */ - public function getRoot() - { - $map = reset($this->_queryComponents); - - if ( ! isset($map['table'])) { - throw new Doctrine_Query_Exception('Root component not initialized.'); - } - - return $map['table']; - } - - /** - * generateTableAlias - * generates a table alias from given table name and associates - * it with given component alias - * - * @param string $componentAlias the component alias to be associated with generated table alias - * @param string $tableName the table name from which to generate the table alias - * @return string the generated table alias - * @deprecated - */ - public function generateTableAlias($componentAlias, $tableName) - { - return $this->generateSqlTableAlias($componentAlias, $tableName); - } - - /** - * generateSqlTableAlias - * generates a table alias from given table name and associates - * it with given component alias - * - * @param string $componentAlias the component alias to be associated with generated table alias - * @param string $tableName the table name from which to generate the table alias - * @return string the generated table alias - */ - public function generateSqlTableAlias($componentAlias, $tableName) - { - $char = strtolower(substr($tableName, 0, 1)); - - $alias = $char; - - if ( ! isset($this->_tableAliasSeeds[$alias])) { - $this->_tableAliasSeeds[$alias] = 1; - } - - while (isset($this->_tableAliasMap[$alias])) { - if ( ! isset($this->_tableAliasSeeds[$alias])) { - $this->_tableAliasSeeds[$alias] = 1; - } - $alias = $char . ++$this->_tableAliasSeeds[$alias]; - } - - $this->_tableAliasMap[$alias] = $componentAlias; - - return $alias; - } - - /** - * getComponentAlias - * get component alias associated with given table alias - * - * @param string $sqlTableAlias the SQL table alias that identifies the component alias - * @return string component alias - */ - public function getComponentAlias($sqlTableAlias) - { - if ( ! isset($this->_tableAliasMap[$sqlTableAlias])) { - throw new Doctrine_Query_Exception('Unknown table alias ' . $sqlTableAlias); - } - return $this->_tableAliasMap[$sqlTableAlias]; - } - - /** - * _execute - * - * @param array $params - * @return PDOStatement The executed PDOStatement. - */ - protected function _execute($params) - { - $params = $this->_conn->convertBooleans($params); - - if ( ! $this->_view) { - if ($this->_queryCache || $this->_conn->getAttribute(Doctrine::ATTR_QUERY_CACHE)) { - $queryCacheDriver = $this->getQueryCacheDriver(); - // calculate hash for dql query - $dql = $this->getDql(); - $hash = md5($dql . 'DOCTRINE_QUERY_CACHE_SALT'); - $cached = $queryCacheDriver->fetch($hash); - if ($cached) { - $query = $this->_constructQueryFromCache($cached); - } else { - $query = $this->getSqlQuery($params); - $serializedQuery = $this->getCachedForm($query); - $queryCacheDriver->save($hash, $serializedQuery, $this->_queryCacheTTL); - } - } else { - $query = $this->getSqlQuery($params); - } - } else { - $query = $this->_view->getSelectSql(); - } - - $params = $this->convertEnums($params); - - if ($this->isLimitSubqueryUsed() && - $this->_conn->getAttribute(Doctrine::ATTR_DRIVER_NAME) !== 'mysql') { - $params = array_merge($params, $params); - } - - if ($this->_type !== self::SELECT) { - return $this->_conn->exec($query, $params); - } - - $stmt = $this->_conn->execute($query, $params); - - return $stmt; + return $this->_type; } + /** - * execute - * executes the query and populates the data set + * Returns the state of this query object + * By default the type is Doctrine_Query_Abstract::STATE_CLEAN but if it appears any unprocessed DQL + * part, it is switched to Doctrine_Query_Abstract::STATE_DIRTY. * - * @param string $params - * @return Doctrine_Collection the root collection - */ - public function execute($params = array(), $hydrationMode = null) - { - $params = array_merge($this->_params['join'], - $this->_params['set'], - $this->_params['where'], - $this->_params['having'], - $params); - - if ($this->_resultCache) { - $cacheDriver = $this->getResultCacheDriver(); - - $dql = $this->getDql(); - // calculate hash for dql query - $hash = md5($dql . var_export($params, true)); - - $cached = ($this->_expireResultCache) ? false : $cacheDriver->fetch($hash); - - if ($cached === false) { - // cache miss - $stmt = $this->_execute($params); - $result = $this->_hydrator->hydrateResultSet( - $this->_createParserResult($stmt, $this->_queryComponents, - $this->_tableAliasMap, Doctrine::HYDRATE_ARRAY)); - - $cached = $this->getCachedForm($result); - $cacheDriver->save($hash, $cached, $this->_resultCacheTTL); - return $result; - } else { - return $this->_constructQueryFromCache($cached); - } - } else { - $stmt = $this->_execute($params); - - if (is_integer($stmt)) { - return $stmt; - } - - return $this->_hydrator->hydrateResultSet( - $this->_createParserResult($stmt, $this->_queryComponents, - $this->_tableAliasMap, $hydrationMode)); - } - } - - /** - * Creates a parser result object. + * @see Doctrine_Query_Abstract::STATE_CLEAN + * @see Doctrine_Query_Abstract::STATE_DIRTY * - * @param unknown_type $stmt - * @param unknown_type $queryComponents - * @param unknown_type $tableToClassAliasMap - * @param unknown_type $hydrationMode - * @return unknown + * @return integer Return the query state */ - private function _createParserResult($stmt, $queryComponents, $tableToClassAliasMap, - $hydrationMode) + public function getState() { - $parserResult = new Doctrine_Query_ParserResultDummy(); - $parserResult->setDatabaseStatement($stmt); - $parserResult->setHydrationMode($hydrationMode); - $parserResult->setQueryComponents($queryComponents); - $parserResult->setTableToClassAliasMap($tableToClassAliasMap); - return $parserResult; + return $this->_state; } - - /** - * Constructs the query from the cached form. - * - * @param string The cached query, in a serialized form. - * @return array The custom component that was cached together with the essential - * query data. This can be either a result set (result caching) - * or an SQL query string (query caching). - */ - protected function _constructQueryFromCache($cached) - { - $cached = unserialize($cached); - $this->_tableAliasMap = $cached[2]; - $customComponent = $cached[0]; - $queryComponents = array(); - $cachedComponents = $cached[1]; - foreach ($cachedComponents as $alias => $components) { - $e = explode('.', $components[0]); - if (count($e) === 1) { - $queryComponents[$alias]['mapper'] = $this->_conn->getMapper($e[0]); - $queryComponents[$alias]['table'] = $queryComponents[$alias]['mapper']->getClassMetadata(); - } else { - $queryComponents[$alias]['parent'] = $e[0]; - $queryComponents[$alias]['relation'] = $queryComponents[$e[0]]['table']->getRelation($e[1]); - $queryComponents[$alias]['mapper'] = $this->_conn->getMapper($queryComponents[$alias]['relation']->getForeignComponentName()); - $queryComponents[$alias]['table'] = $queryComponents[$alias]['mapper']->getClassMetadata(); - } - if (isset($components[1])) { - $queryComponents[$alias]['agg'] = $components[1]; - } - if (isset($components[2])) { - $queryComponents[$alias]['map'] = $components[2]; - } - } - $this->_queryComponents = $queryComponents; - - return $customComponent; - } - - /** - * getCachedForm - * returns the cached form of this query for given resultSet - * - * @param array $resultSet - * @return string serialized string representation of this query - */ - public function getCachedForm($customComponent = null) - { - $componentInfo = array(); - foreach ($this->getQueryComponents() as $alias => $components) { - if ( ! isset($components['parent'])) { - $componentInfo[$alias][] = $components['mapper']->getComponentName(); - //$componentInfo[$alias][] = $components['mapper']->getComponentName(); - } else { - $componentInfo[$alias][] = $components['parent'] . '.' . $components['relation']->getAlias(); - } - if (isset($components['agg'])) { - $componentInfo[$alias][] = $components['agg']; - } - if (isset($components['map'])) { - $componentInfo[$alias][] = $components['map']; - } - } - - return serialize(array($customComponent, $componentInfo, $this->getTableAliasMap())); - } - /** - * addSelect - * adds fields to the SELECT part of the query + * Adds fields to the SELECT part of the query * - * @param string $select Query SELECT part + * @param string $select Query SELECT part * @return Doctrine_Query */ - public function addSelect($select) + public function select($select = '', $override = false) { - return $this->_addDqlQueryPart('select', $select, true); - } - - /** - * addTableAlias - * adds an alias for table and associates it with given component alias - * - * @param string $componentAlias the alias for the query component associated with given tableAlias - * @param string $tableAlias the table alias to be added - * @return Doctrine_Hydrate - * @deprecated - */ - public function addTableAlias($tableAlias, $componentAlias) - { - return $this->addSqlTableAlias($tableAlias, $componentAlias); - } - - /** - * addSqlTableAlias - * adds an SQL table alias and associates it a component alias - * - * @param string $componentAlias the alias for the query component associated with given tableAlias - * @param string $tableAlias the table alias to be added - * @return Doctrine_Query_Abstract - */ - public function addSqlTableAlias($sqlTableAlias, $componentAlias) - { - $this->_tableAliasMap[$sqlTableAlias] = $componentAlias; - return $this; - } - - /** - * addFrom - * adds fields to the FROM part of the query - * - * @param string $from Query FROM part - * @return Doctrine_Query - */ - public function addFrom($from) - { - return $this->_addDqlQueryPart('from', $from, true); - } - - /** - * addWhere - * adds conditions to the WHERE part of the query - * - * @param string $where Query WHERE part - * @param mixed $params an array of parameters or a simple scalar - * @return Doctrine_Query - */ - public function addWhere($where, $params = array()) - { - if (is_array($params)) { - $this->_params['where'] = array_merge($this->_params['where'], $params); - } else { - $this->_params['where'][] = $params; - } - return $this->_addDqlQueryPart('where', $where, true); - } - - /** - * whereIn - * adds IN condition to the query WHERE part - * - * @param string $expr the operand of the IN - * @param mixed $params an array of parameters or a simple scalar - * @param boolean $not whether or not to use NOT in front of IN - * @return Doctrine_Query - */ - public function whereIn($expr, $params = array(), $not = false) - { - $params = (array) $params; - - // if there's no params, return (else we'll get a WHERE IN (), invalid SQL) - if (!count($params)) - return $this; - - $a = array(); - foreach ($params as $k => $value) { - if ($value instanceof Doctrine_Expression) { - $value = $value->getSql(); - unset($params[$k]); - } else { - $value = '?'; - } - $a[] = $value; + if ($select === '') { + return $this; } - $this->_params['where'] = array_merge($this->_params['where'], $params); - - $where = $expr . ($not === true ? ' NOT ':'') . ' IN (' . implode(', ', $a) . ')'; - - return $this->_addDqlQueryPart('where', $where, true); + return $this->_addDqlQueryPart('select', $select, ! $override); } - /** - * whereNotIn - * adds NOT IN condition to the query WHERE part - * - * @param string $expr the operand of the NOT IN - * @param mixed $params an array of parameters or a simple scalar - * @return Doctrine_Query - */ - public function whereNotIn($expr, $params = array()) - { - return $this->whereIn($expr, $params, true); - } /** - * addGroupBy - * adds fields to the GROUP BY part of the query - * - * @param string $groupby Query GROUP BY part - * @return Doctrine_Query - */ - public function addGroupBy($groupby) - { - return $this->_addDqlQueryPart('groupby', $groupby, true); - } - - /** - * addHaving - * adds conditions to the HAVING part of the query - * - * @param string $having Query HAVING part - * @param mixed $params an array of parameters or a simple scalar - * @return Doctrine_Query - */ - public function addHaving($having, $params = array()) - { - if (is_array($params)) { - $this->_params['having'] = array_merge($this->_params['having'], $params); - } else { - $this->_params['having'][] = $params; - } - return $this->_addDqlQueryPart('having', $having, true); - } - - /** - * addOrderBy - * adds fields to the ORDER BY part of the query - * - * @param string $orderby Query ORDER BY part - * @return Doctrine_Query - */ - public function addOrderBy($orderby) - { - return $this->_addDqlQueryPart('orderby', $orderby, true); - } - - /** - * select - * sets the SELECT part of the query - * - * @param string $select Query SELECT part - * @return Doctrine_Query - */ - public function select($select) - { - return $this->_addDqlQueryPart('select', $select); - } - - /** - * distinct * Makes the query SELECT DISTINCT. * - * @param bool $flag Whether or not the SELECT is DISTINCT (default true). + * @param bool $flag Whether or not the SELECT is DISTINCT (default true). * @return Doctrine_Query */ public function distinct($flag = true) - { - $this->_sqlParts['distinct'] = (bool) $flag; + { + $this->_dqlParts['distinct'] = (bool) $flag; return $this; } + /** - * forUpdate * Makes the query SELECT FOR UPDATE. * - * @param bool $flag Whether or not the SELECT is FOR UPDATE (default true). + * @param bool $flag Whether or not the SELECT is FOR UPDATE (default true). * @return Doctrine_Query + * + * @todo [TODO] What about "forUpdate" support? Remove it? */ public function forUpdate($flag = true) { - $this->_sqlParts[self::FOR_UPDATE] = (bool) $flag; - return $this; + return $this->_addDqlQueryPart('forUpdate', (bool) $flag); } + /** - * delete - * sets the query type to DELETE + * Sets the query type to DELETE * * @return Doctrine_Query */ @@ -1309,11 +423,11 @@ abstract class Doctrine_Query_Abstract return $this; } + /** - * update - * sets the UPDATE part of the query + * Sets the UPDATE part of the query * - * @param string $update Query UPDATE part + * @param string $update Query UPDATE part * @return Doctrine_Query */ public function update($update) @@ -1322,19 +436,22 @@ abstract class Doctrine_Query_Abstract return $this->_addDqlQueryPart('from', $update); } + /** - * set - * sets the SET part of the query + * Sets the SET part of the query * - * @param string $update Query UPDATE part + * @param mixed $key UPDATE keys. Accepts either a string (requiring then $value or $params to be defined) + * or an array of $key => $value pairs. + * @param string $value UPDATE key value. Optional argument, but required if $key is a string. * @return Doctrine_Query */ - public function set($key, $value, $params = null) + public function set($key, $value = null, $params = null) { if (is_array($key)) { foreach ($key as $k => $v) { - $this->set($k, '?', array($v)); + $this->set($k, '?', array($v)); } + return $this; } else { if ($params !== null) { @@ -1344,27 +461,32 @@ abstract class Doctrine_Query_Abstract $this->_params['set'][] = $params; } } + + if ($value === null) { + throw new Doctrine_Query_Exception( 'Cannot try to set \''.$key.'\' without a value.' ); + } + return $this->_addDqlQueryPart('set', $key . ' = ' . $value, true); } } /** - * from - * sets the FROM part of the query + * Adds fields to the FROM part of the query * - * @param string $from Query FROM part + * @param string $from Query FROM part * @return Doctrine_Query */ - public function from($from) + public function from($from, $override = false) { - return $this->_addDqlQueryPart('from', $from); + return $this->_addDqlQueryPart('from', $from, ! $override); } + /** - * innerJoin - * appends an INNER JOIN to the FROM part of the query + * Appends an INNER JOIN to the FROM part of the query * - * @param string $join Query INNER JOIN + * @param string $join Query INNER JOIN + * @param mixed $params Optional JOIN params (array of parameters or a simple scalar) * @return Doctrine_Query */ public function innerJoin($join, $params = array()) @@ -1378,11 +500,25 @@ abstract class Doctrine_Query_Abstract return $this->_addDqlQueryPart('from', 'INNER JOIN ' . $join, true); } + /** - * leftJoin - * appends a LEFT JOIN to the FROM part of the query + * Appends an INNER JOIN to the FROM part of the query * - * @param string $join Query LEFT JOIN + * @param string $join Query INNER JOIN + * @param mixed $params Optional JOIN params (array of parameters or a simple scalar) + * @return Doctrine_Query + */ + public function join($join, $params = array()) + { + return $this->innerJoin($join, $params); + } + + + /** + * Appends a LEFT JOIN to the FROM part of the query + * + * @param string $join Query LEFT JOIN + * @param mixed $params Optional JOIN params (array of parameters or a simple scalar) * @return Doctrine_Query */ public function leftJoin($join, $params = array()) @@ -1396,75 +532,257 @@ abstract class Doctrine_Query_Abstract return $this->_addDqlQueryPart('from', 'LEFT JOIN ' . $join, true); } - /** - * groupBy - * sets the GROUP BY part of the query - * - * @param string $groupby Query GROUP BY part - * @return Doctrine_Query - */ - public function groupBy($groupby) - { - return $this->_addDqlQueryPart('groupby', $groupby); - } /** - * where - * sets the WHERE part of the query + * Adds conditions to the WHERE part of the query * - * @param string $join Query WHERE part - * @param mixed $params an array of parameters or a simple scalar + * @param string $where Query WHERE part + * @param mixed $params An array of parameters or a simple scalar * @return Doctrine_Query */ - public function where($where, $params = array()) + public function where($where, $params = array(), $override = false) { - $this->_params['where'] = array(); + if ($override) { + $this->_params['where'] = array(); + } + if (is_array($params)) { - $this->_params['where'] = $params; + $this->_params['where'] = array_merge($this->_params['where'], $params); } else { $this->_params['where'][] = $params; } - return $this->_addDqlQueryPart('where', $where); + return $this->_addDqlQueryPart('where', $where, ! $override); } + /** - * having - * sets the HAVING part of the query + * Adds conditions to the WHERE part of the query * - * @param string $having Query HAVING part - * @param mixed $params an array of parameters or a simple scalar + * @param string $where Query WHERE part + * @param mixed $params An array of parameters or a simple scalar * @return Doctrine_Query */ - public function having($having, $params = array()) + public function andWhere($where, $params = array(), $override = false) { - $this->_params['having'] = array(); + if (count($this->getDqlQueryPart('where')) > 0) { + $this->_addDqlQueryPart('where', 'AND', true); + } + + return $this->where($where, $params, $override); + } + + + /** + * Adds conditions to the WHERE part of the query + * + * @param string $where Query WHERE part + * @param mixed $params An array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function orWhere($where, $params = array(), $override = false) + { + if (count($this->getDqlQueryPart('where')) > 0) { + $this->_addDqlQueryPart('where', 'OR', true); + } + + return $this->where($where, $params, $override); + } + + + /** + * Adds IN condition to the query WHERE part + * + * @param string $expr The operand of the IN + * @param mixed $params An array of parameters or a simple scalar + * @param boolean $not Whether or not to use NOT in front of IN + * @return Doctrine_Query + */ + public function whereIn($expr, $params = array(), $override = false, $not = false) + { + $params = (array) $params; + + // Must have at least one param, otherwise we'll get an empty IN () => invalid SQL + if ( ! count($params)) { + return $this; + } + + list($sqlPart, $params) = $this->_processWhereInParams($params); + + $where = $expr . ($not === true ? ' NOT' : '') . ' IN (' . $sqlPart . ')'; + + return $this->_returnWhereIn($where, $params, $override); + } + + + /** + * Adds NOT IN condition to the query WHERE part + * + * @param string $expr The operand of the NOT IN + * @param mixed $params An array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function whereNotIn($expr, $params = array(), $override = false) + { + return $this->whereIn($expr, $params, $override, true); + } + + + /** + * Adds IN condition to the query WHERE part + * + * @param string $expr The operand of the IN + * @param mixed $params An array of parameters or a simple scalar + * @param boolean $not Whether or not to use NOT in front of IN + * @return Doctrine_Query + */ + public function andWhereIn($expr, $params = array(), $override = false) + { + if (count($this->getDqlQueryPart('where')) > 0) { + $this->_addDqlQueryPart('where', 'AND', true); + } + + return $this->whereIn($expr, $params, $override); + } + + + /** + * Adds NOT IN condition to the query WHERE part + * + * @param string $expr The operand of the NOT IN + * @param mixed $params An array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function andWhereNotIn($expr, $params = array(), $override = false) + { + if (count($this->getDqlQueryPart('where')) > 0) { + $this->_addDqlQueryPart('where', 'AND', true); + } + + return $this->whereIn($expr, $params, $override, true); + } + + + /** + * Adds IN condition to the query WHERE part + * + * @param string $expr The operand of the IN + * @param mixed $params An array of parameters or a simple scalar + * @param boolean $not Whether or not to use NOT in front of IN + * @return Doctrine_Query + */ + public function orWhereIn($expr, $params = array(), $override = false) + { + if (count($this->getDqlQueryPart('where')) > 0) { + $this->_addDqlQueryPart('where', 'OR', true); + } + + return $this->whereIn($expr, $params, $override); + } + + + /** + * Adds NOT IN condition to the query WHERE part + * + * @param string $expr The operand of the NOT IN + * @param mixed $params An array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function orWhereNotIn($expr, $params = array(), $override = false) + { + if (count($this->getDqlQueryPart('where')) > 0) { + $this->_addDqlQueryPart('where', 'OR', true); + } + + return $this->whereIn($expr, $params, $override, true); + } + + + /** + * Adds fields to the GROUP BY part of the query + * + * @param string $groupby Query GROUP BY part + * @return Doctrine_Query + */ + public function groupBy($groupby, $override = false) + { + return $this->_addDqlQueryPart('groupby', $groupby, ! $override); + } + + + /** + * Adds conditions to the HAVING part of the query + * + * @param string $having Query HAVING part + * @param mixed $params An array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function having($having, $params = array(), $override = false) + { + if ($override) { + $this->_params['having'] = array(); + } + if (is_array($params)) { - $this->_params['having'] = $params; + $this->_params['having'] = array_merge($this->_params['having'], $params); } else { $this->_params['having'][] = $params; } - - return $this->_addDqlQueryPart('having', $having); + + return $this->_addDqlQueryPart('having', $having, true); } + /** - * orderBy - * sets the ORDER BY part of the query + * Adds conditions to the HAVING part of the query * - * @param string $orderby Query ORDER BY part + * @param string $having Query HAVING part + * @param mixed $params An array of parameters or a simple scalar * @return Doctrine_Query */ - public function orderBy($orderby) + public function andHaving($having, $params = array(), $override = false) { - return $this->_addDqlQueryPart('orderby', $orderby); + if (count($this->getDqlQueryPart('having')) > 0) { + $this->_addDqlQueryPart('having', 'AND', true); + } + + return $this->having($having, $params, $override); } + /** - * limit - * sets the Query query limit + * Adds conditions to the HAVING part of the query * - * @param integer $limit limit to be used for limiting the query results + * @param string $having Query HAVING part + * @param mixed $params An array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function orHaving($having, $params = array(), $override = false) + { + if (count($this->getDqlQueryPart('having')) > 0) { + $this->_addDqlQueryPart('having', 'OR', true); + } + + return $this->having($having, $params, $override); + } + + + /** + * Adds fields to the ORDER BY part of the query + * + * @param string $orderby Query ORDER BY part + * @return Doctrine_Query + */ + public function orderBy($orderby, $override = false) + { + return $this->_addDqlQueryPart('orderby', $orderby, ! $override); + } + + + /** + * Sets the Query query limit + * + * @param integer $limit Limit to be used for limiting the query results * @return Doctrine_Query */ public function limit($limit) @@ -1472,299 +790,123 @@ abstract class Doctrine_Query_Abstract return $this->_addDqlQueryPart('limit', $limit); } + /** - * offset - * sets the Query query offset + * Sets the Query query offset * - * @param integer $offset offset to be used for paginating the query + * @param integer $offset Offset to be used for paginating the query * @return Doctrine_Query */ public function offset($offset) { return $this->_addDqlQueryPart('offset', $offset); } - - /** - * getSql - * shortcut for {@link getSqlQuery()}. - * - * @return string sql query string - */ - public function getSql() - { - return $this->getSqlQuery(); - } - - /** - * clear - * resets all the variables - * - * @return void - */ - protected function clear() - { - $this->_sqlParts = array( - 'select' => array(), - 'distinct' => false, - 'forUpdate' => false, - 'from' => array(), - 'set' => array(), - 'join' => array(), - 'where' => array(), - 'groupby' => array(), - 'having' => array(), - 'orderby' => array(), - 'limit' => false, - 'offset' => false, - ); - } - - public function setHydrationMode($hydrationMode) - { - $this->_hydrator->setHydrationMode($hydrationMode); - return $this; - } - - /** - * @deprecated - */ - public function getAliasMap() - { - return $this->_queryComponents; - } - - /** - * Gets the components of this query. - */ - public function getQueryComponents() - { - return $this->_queryComponents; - } - - /** - * Return the SQL parts. - * - * @return array The parts - * @deprecated - */ - public function getParts() - { - return $this->getSqlParts(); - } - - /** - * Return the SQL parts. - * - * @return array The parts - */ - public function getSqlParts() - { - return $this->_sqlParts; - } + /** - * getType + * Set enumerated parameters * - * returns the type of this query object - * by default the type is Doctrine_Query_Abstract::SELECT but if update() or delete() - * are being called the type is Doctrine_Query_Abstract::UPDATE and Doctrine_Query_Abstract::DELETE, - * respectively - * - * @see Doctrine_Query_Abstract::SELECT - * @see Doctrine_Query_Abstract::UPDATE - * @see Doctrine_Query_Abstract::DELETE - * - * @return integer return the query type + * @param array $enumParams Enum parameters. */ - public function getType() + protected function _setEnumParams($enumParams = array()) { - return $this->_type; + $this->_enumParams = $enumParams; } - - /** - * useCache - * - * @param Doctrine_Cache_Interface|bool $driver cache driver - * @param integer $timeToLive how long the cache entry is valid - * @return Doctrine_Hydrate this object - * @deprecated Use useResultCache() - */ - public function useCache($driver = true, $timeToLive = null) - { - return $this->useResultCache($driver, $timeToLive); - } - - /** - * useResultCache - * - * @param Doctrine_Cache_Interface|bool $driver cache driver - * @param integer $timeToLive how long the cache entry is valid - * @return Doctrine_Hydrate this object - */ - public function useResultCache($driver = true, $timeToLive = null) - { - if ($driver !== null && $driver !== true && ! ($driver instanceOf Doctrine_Cache_Interface)){ - $msg = 'First argument should be instance of Doctrine_Cache_Interface or null.'; - throw new Doctrine_Query_Exception($msg); - } - $this->_resultCache = $driver; - return $this->setResultCacheLifeSpan($timeToLive); - } - - /** - * useQueryCache - * - * @param Doctrine_Cache_Interface|bool $driver cache driver - * @param integer $timeToLive how long the cache entry is valid - * @return Doctrine_Hydrate this object - */ - public function useQueryCache(Doctrine_Cache_Interface $driver, $timeToLive = null) - { - $this->_queryCache = $driver; - return $this->setQueryCacheLifeSpan($timeToLive); - } - - /** - * expireCache - * - * @param boolean $expire whether or not to force cache expiration - * @return Doctrine_Hydrate this object - * @deprecated Use expireResultCache() - */ - public function expireCache($expire = true) - { - return $this->expireResultCache($expire); - } - - /** - * expireCache - * - * @param boolean $expire whether or not to force cache expiration - * @return Doctrine_Hydrate this object - */ - public function expireResultCache($expire = true) - { - $this->_expireResultCache = true; - return $this; - } - - /** - * expireQueryCache - * - * @param boolean $expire whether or not to force cache expiration - * @return Doctrine_Hydrate this object - */ - public function expireQueryCache($expire = true) - { - $this->_expireQueryCache = true; - return $this; - } - - /** - * setCacheLifeSpan - * - * @param integer $timeToLive how long the cache entry is valid - * @return Doctrine_Hydrate this object - * @deprecated Use setResultCacheLifeSpan() - */ - public function setCacheLifeSpan($timeToLive) - { - return $this->setResultCacheLifeSpan($timeToLive); - } - - /** - * setResultCacheLifeSpan - * - * @param integer $timeToLive how long the cache entry is valid - * @return Doctrine_Hydrate this object - */ - public function setResultCacheLifeSpan($timeToLive) - { - if ($timeToLive !== null) { - $timeToLive = (int) $timeToLive; - } - $this->_resultCacheTTL = $timeToLive; - return $this; - } - /** - * setQueryCacheLifeSpan + * Get all enumerated parameters * - * @param integer $timeToLive how long the cache entry is valid - * @return Doctrine_Hydrate this object + * @return array All enumerated parameters */ - public function setQueryCacheLifeSpan($timeToLive) + public function getEnumParams() { - if ($timeToLive !== null) { - $timeToLive = (int) $timeToLive; - } - $this->_queryCacheTTL = $timeToLive; + return $this->_enumParams; + } + - return $this; - } - /** - * getCacheDriver - * returns the cache driver associated with this object + * Convert ENUM parameters to their integer equivalents * - * @return Doctrine_Cache_Interface|boolean|null cache driver - * @deprecated Use getResultCacheDriver() + * @param $params Parameters to be converted + * @return array Converted parameters array */ - public function getCacheDriver() + public function convertEnums($params) { - return $this->getResultCacheDriver(); - } - - /** - * getResultCacheDriver - * returns the cache driver used for caching result sets - * - * @return Doctrine_Cache_Interface|boolean|null cache driver - */ - public function getResultCacheDriver() - { - if ($this->_resultCache instanceof Doctrine_Cache_Interface) { - return $this->_resultCache; - } else { - return $this->_conn->getResultCacheDriver(); + foreach ($this->_enumParams as $key => $values) { + if (isset($params[$key]) && ! empty($values)) { + $params[$key] = $values[0]->enumIndex($values[1], $params[$key]); + } } + + return $params; } - + + /** - * getQueryCacheDriver - * returns the cache driver used for caching queries + * Get all defined parameters * - * @return Doctrine_Cache_Interface|boolean|null cache driver + * @return array Defined parameters */ - public function getQueryCacheDriver() + public function getParams($params = array()) { - if ($this->_queryCache instanceof Doctrine_Cache_Interface) { - return $this->_queryCache; - } else { - return $this->_conn->getQueryCacheDriver(); + return array_merge( + $this->_params['join'], + $this->_params['set'], + $this->_params['where'], + $this->_params['having'], + $params + ); + } + + + /** + * setParams + * + * @param array $params + */ + public function setParams(array $params = array()) { + $this->_params = $params; + } + + + /** + * Method to check if a arbitrary piece of DQL exists + * + * @param string $dql Arbitrary piece of DQL to check for + * @return boolean + */ + public function contains($dql) + { + return stripos($this->getDql(), $dql) === false ? false : true; + } + + + /** + * Retrieve a DQL part for internal purposes + * + * @param string $queryPartName The name of the query part. + * @return mixed Array related to query part or simple scalar + */ + public function getDqlQueryPart($queryPartName) + { + if ( ! isset($this->_dqlParts[$queryPartName])) { + throw new Doctrine_Query_Exception('Unknown DQL query part \'' . $queryPartName . '\''); } + + return $this->_dqlParts[$queryPartName]; } - + + /** - * getConnection - * - * @return Doctrine_Connection - */ - public function getConnection() - { - return $this->_conn; - } - - /** * Adds a DQL part to the internal parts collection. - * + * * @param string $queryPartName The name of the query part. * @param string $queryPart The actual query part to add. * @param boolean $append Whether to append $queryPart to already existing * parts under the same $queryPartName. Defaults to FALSE * (previously added parts with the same name get overridden). + * @return Doctrine_Query */ protected function _addDqlQueryPart($queryPartName, $queryPart, $append = false) { @@ -1773,96 +915,77 @@ abstract class Doctrine_Query_Abstract } else { $this->_dqlParts[$queryPartName] = array($queryPart); } - + $this->_state = Doctrine_Query::STATE_DIRTY; - return $this; + return $this; } - + + /** - * _processDqlQueryPart - * parses given query part + * Processes the WHERE IN () parameters and return an indexed array containing + * the sqlPart to be placed in SQL statement and the new parameters (that will be + * bound in SQL execution) * - * @param string $queryPartName the name of the query part - * @param array $queryParts an array containing the query part data - * @return Doctrine_Query this object - * @todo Better description. "parses given query part" ??? Then wheres the difference - * between process/parseQueryPart? I suppose this does something different. + * @param array $params Parameters to be processed + * @return array */ - protected function _processDqlQueryPart($queryPartName, $queryParts) + protected function _processWhereInParams($params = array()) { - $this->removeSqlQueryPart($queryPartName); - - if (is_array($queryParts) && ! empty($queryParts)) { - foreach ($queryParts as $queryPart) { - $parser = $this->_getParser($queryPartName); - $sql = $parser->parse($queryPart); - if (isset($sql)) { - if ($queryPartName == 'limit' || $queryPartName == 'offset') { - $this->setSqlQueryPart($queryPartName, $sql); - } else { - $this->addSqlQueryPart($queryPartName, $sql); - } - } - } - } + return array( + // [0] => sqlPart + implode(', ', array_map(array(&$this, '_processWhereInSqlPart'), $params)), + // [1] => params + array_filter($params, array(&$this, '_processWhereInParamItem')), + ); } - + + /** - * _getParser - * parser lazy-loader - * - * @throws Doctrine_Query_Exception if unknown parser name given - * @return Doctrine_Query_Part - * @todo Doc/Description: What is the parameter for? Which parsers are available? + * @nodoc */ - protected function _getParser($name) + protected function _processWhereInSqlPart($value) { - if ( ! isset($this->_parsers[$name])) { - $class = 'Doctrine_Query_' . ucwords(strtolower($name)); - - Doctrine::autoload($class); - - if ( ! class_exists($class)) { - throw new Doctrine_Query_Exception('Unknown parser ' . $name); - } - - $this->_parsers[$name] = new $class($this, $this->_tokenizer); - } - - return $this->_parsers[$name]; + // [TODO] Add support to imbricated query (must deliver the hardest effort to Parser) + return ($value instanceof Doctrine_Expression) ? $value->getSql() : '?'; } - + + + /** + * @nodoc + */ + protected function _processWhereInParamItem($value) + { + // [TODO] Add support to imbricated query (must deliver the hardest effort to Parser) + return ( ! ($value instanceof Doctrine_Expression)); + } + + + /** + * Processes a WHERE IN () and build defined stuff to add in DQL + * + * @param string $where The WHERE clause to be added + * @param array $params WHERE clause parameters + * @param mixed $appender Where this clause may be not be appended, or appended + * (two possible values: AND or OR) + * @return Doctrine_Query + */ + protected function _returnWhereIn($where, $params = array(), $override = false) + { + // Parameters inclusion + $this->_params['where'] = $override ? $params : array_merge($this->_params['where'], $params); + + // WHERE clause definition + return $this->_addDqlQueryPart('where', $where, ! $override); + } + + /** * Gets the SQL query that corresponds to this query object. - * The returned SQL syntax depends on the connection driver that is used + * The returned SQL syntax depends on the connection driver that is used * by this query object at the time of this method call. * - * @param array $params + * @return string SQL query */ - abstract public function getSqlQuery($params = array()); - - /** - * parseDqlQuery - * parses a dql query - * - * @param string $query query to be parsed - * @return Doctrine_Query_Abstract this object - */ - abstract public function parseDqlQuery($query); - - /** - * @deprecated - */ - public function parseQuery($query) - { - return $this->parseDqlQuery($query); - } - - /** - * @deprecated - */ - public function getQuery($params = array()) - { - return $this->getSqlQuery($params); - } + abstract public function getSql(); + } diff --git a/lib/Doctrine/Query/AbstractResult.php b/lib/Doctrine/Query/AbstractResult.php new file mode 100755 index 000000000..53e9d5ecc --- /dev/null +++ b/lib/Doctrine/Query/AbstractResult.php @@ -0,0 +1,266 @@ +. + */ + +/** + * Doctrine_Query_AbstractResult + * + * @package Doctrine + * @subpackage Query + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision: 1393 $ + * @author Guilherme Blanco + * @author Konsta Vesterinen + */ +abstract class Doctrine_Query_AbstractResult +{ + /** + * @var mixed $_data The actual data to be stored. Can be an array, a string or an integer. + */ + protected $_data; + + /** + * @var array $_queryComponents + * + * Two dimensional array containing the map for query aliases. Main keys are component aliases. + * + * table Table object associated with given alias. + * relation Relation object owned by the parent. + * parent Alias of the parent. + * agg Aggregates of this component. + * map Name of the column / aggregate value this component is mapped to a collection. + */ + protected $_queryComponents; + + /** + * @var array Table alias map. Keys are SQL aliases and values DQL aliases. + */ + protected $_tableAliasMap; + + /** + * @var array Enum params. + */ + protected $_enumParams; + + + /** + * Cannot be called directly, factory methods handle this job. + * + * @param mixed $data Data to be stored. + * @param array $queryComponents Query components. + * @param array $tableAliasMap Table aliases. + * @param array $enumParams Enum params. + * @return Doctrine_Query_CacheHandler + */ + public function __construct($data = '', $queryComponents = array(), $tableAliasMap = array(), $enumParams = array()) + { + $this->_data = $data; + $this->_queryComponents = $queryComponents; + $this->_tableAliasMap = $tableAliasMap; + $this->_enumParams = $enumParams; + } + + + /** + * Defines the mapping components. + * + * @param array $queryComponents Query components. + */ + public function setQueryComponents(array $queryComponents) + { + $this->_queryComponents = $queryComponents; + } + + + /** + * Sets the declaration for given component alias. + * + * @param string $componentAlias The component alias to set the declaration to. + * @param string $queryComponent Alias declaration. + */ + public function setQueryComponent($componentAlias, array $queryComponent) + { + $this->_queryComponents[$componentAlias] = $queryComponent; + } + + + /** + * Gets the mapping components. + * + * @return array Query components. + */ + public function getQueryComponents() + { + return $this->_queryComponents; + } + + + /** + * Get the declaration for given component alias. + * + * @param string $componentAlias The component alias the retrieve the declaration from. + * @return array Alias declaration. + */ + public function getQueryComponent($componentAlias) + { + if ( ! isset($this->_queryComponents[$componentAlias])) { + throw new Doctrine_Query_Exception('Unknown query component ' . $componentAlias); + } + + return $this->_queryComponents[$componentAlias]; + } + + + /** + * Whether or not this object has a declaration for given component alias. + * + * @param string $componentAlias Component alias the retrieve the declaration from. + * @return boolean True if this object has given alias, otherwise false. + */ + public function hasQueryComponent($componentAlias) + { + return isset($this->_queryComponents[$componentAlias]); + } + + + /** + * Defines the table aliases. + * + * @param array $tableAliasMap Table aliases. + */ + public function setTableAliasMap(array $tableAliasMap) + { + $this->_tableAliasMap = $tableAliasMap; + } + + + /** + * Adds an SQL table alias and associates it a component alias + * + * @param string $tableAlias Table alias to be added. + * @param string $componentAlias Alias for the query component associated with given tableAlias. + */ + public function setTableAlias($tableAlias, $componentAlias) + { + $this->_tableAliasMap[$tableAlias] = $componentAlias; + } + + + /** + * Returns all table aliases. + * + * @return array Table aliases as an array. + */ + public function getTableAliasMap() + { + return $this->_tableAliasMap; + } + + + /** + * Get component alias associated with given table alias. + * + * @param string $tableAlias SQL table alias that identifies the component alias + * @return string Component alias + */ + public function getTableAlias($tableAlias) + { + if ( ! isset($this->_tableAliasMap[$tableAlias])) { + throw new Doctrine_Query_Exception('Unknown table alias ' . $tableAlias); + } + + return $this->_tableAliasMap[$tableAlias]; + } + + + /** + * Get table alias associated with given component alias. + * + * @param string $componentAlias Component alias that identifies the table alias + * @return string Component alias + */ + public function getTableAliasFromComponentAlias($componentAlias) + { + return array_search($componentAlias, $this->_tableAliasMap); + } + + + /** + * Whether or not this object has given tableAlias. + * + * @param string $tableAlias Table alias to be checked. + * @return boolean True if this object has given alias, otherwise false. + */ + public function hasTableAlias($tableAlias) + { + return (isset($this->_tableAliasMap[$tableAlias])); + } + + + /** + * Returns the enum parameters. + * + * @return mixed Enum parameters. + */ + public function getEnumParams() + { + return $this->_enumParams; + } + + + /** + * Sets input parameter as an enumerated parameter + * + * @param string $key The key of the input parameter + * @return Doctrine_Query_AbstractResult + */ + public function addEnumParam($key, $table = null, $column = null) + { + $array = (isset($table) || isset($column)) ? array($table, $column) : array(); + + if ($key === '?') { + $this->_enumParams[] = $array; + } else { + $this->_enumParams[$key] = $array; + } + + return $this; + } + + + /** + * Returns this object in serialized format, revertable using fromCached*. + * + * @return string Serialized cached item. + */ + public function toCachedForm() + { + return serialize(array( + $this->_data, + $this->getQueryComponents(), + $this->getTableAliasMap(), + $this->getEnumParams() + )); + } + +} \ No newline at end of file diff --git a/lib/Doctrine/Query/Abstract_old.php b/lib/Doctrine/Query/Abstract_old.php new file mode 100644 index 000000000..0d1a26d16 --- /dev/null +++ b/lib/Doctrine/Query/Abstract_old.php @@ -0,0 +1,1805 @@ +. + */ + +/** + * Doctrine_Query_Abstract + * + * @package Doctrine + * @subpackage Query + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.org + * @since 1.0 + * @version $Revision: 1393 $ + * @author Konsta Vesterinen + * @todo See {@link Doctrine_Query} + */ +abstract class Doctrine_Query_Abstract +{ + /** + * QUERY TYPE CONSTANTS + */ + + /** + * constant for SELECT queries + */ + const SELECT = 0; + + /** + * constant for DELETE queries + */ + const DELETE = 1; + + /** + * constant for UPDATE queries + */ + const UPDATE = 2; + + /** + * constant for INSERT queries + */ + const INSERT = 3; + + /** + * constant for CREATE queries + */ + const CREATE = 4; + + /** @todo document the query states (and the transitions between them). */ + /** + * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts. + */ + const STATE_CLEAN = 1; + + /** + * A query object is in state DIRTY when it has DQL parts that have not yet been + * parsed/processed. + */ + const STATE_DIRTY = 2; + + /** + * A query is in DIRECT state when ... ? + */ + const STATE_DIRECT = 3; + + /** + * A query object is on LOCKED state when ... ? + */ + const STATE_LOCKED = 4; + + /** + * @var array Table alias map. Keys are SQL aliases and values DQL aliases. + */ + protected $_tableAliasMap = array(); + + /** + * @var Doctrine_View The view object used by this query, if any. + */ + protected $_view; + + /** + * @var integer $_state The current state of this query. + */ + protected $_state = Doctrine_Query::STATE_CLEAN; + + /** + * @var array $params The parameters of this query. + */ + protected $_params = array('join' => array(), + 'where' => array(), + 'set' => array(), + 'having' => array()); + + /* Caching properties */ + /** + * @var Doctrine_Cache_Interface The cache driver used for caching result sets. + */ + protected $_resultCache; + /** + * @var boolean $_expireResultCache A boolean value that indicates whether or not + * expire the result cache. + */ + protected $_expireResultCache = false; + protected $_resultCacheTTL; + + /** + * @var Doctrine_Cache_Interface The cache driver used for caching queries. + */ + protected $_queryCache; + protected $_expireQueryCache = false; + protected $_queryCacheTTL; + + + /** + * @var Doctrine_Connection The connection used by this query object. + */ + protected $_conn; + + + /** + * @var array $_sqlParts The SQL query string parts. Filled during the DQL parsing process. + */ + protected $_sqlParts = array( + 'select' => array(), + 'distinct' => false, + 'forUpdate' => false, + 'from' => array(), + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => false, + 'offset' => false, + ); + + /** + * @var array $_dqlParts An array containing all DQL query parts. + */ + protected $_dqlParts = array( + 'from' => array(), + 'select' => array(), + 'forUpdate' => false, + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => array(), + 'offset' => array(), + ); + + + /** + * @var array $_queryComponents Two dimensional array containing the components of this query, + * informations about their relations and other related information. + * The components are constructed during query parsing. + * + * Keys are component aliases and values the following: + * + * table table object associated with given alias + * + * relation the relation object owned by the parent + * + * parent the alias of the parent + * + * agg the aggregates of this component + * + * map the name of the column / aggregate value this + * component is mapped to a collection + */ + protected $_queryComponents = array(); + + /** + * @var integer $type the query type + * + * @see Doctrine_Query::* constants + */ + protected $_type = self::SELECT; + + /** + * @var Doctrine_Hydrator The hydrator object used to hydrate query results. + */ + protected $_hydrator; + + /** + * @var Doctrine_Query_Tokenizer The tokenizer that is used during the query parsing process. + */ + protected $_tokenizer; + + /** + * @var array $_tableAliasSeeds A simple array keys representing table aliases and values + * table alias seeds. The seeds are used for generating short table + * aliases. + */ + protected $_tableAliasSeeds = array(); + + /** + * @var array $_options an array of options + */ + protected $_options = array( + 'fetchMode' => Doctrine::FETCH_RECORD + ); + + /** + * @var array $_enumParams an array containing the keys of the parameters that should be enumerated + */ + protected $_enumParams = array(); + + /** + * @var boolean + */ + protected $_isLimitSubqueryUsed = false; + + + /** + * Constructor. + * + * @param Doctrine_Connection The connection object the query will use. + * @param Doctrine_Hydrator_Abstract The hydrator that will be used for generating result sets. + */ + public function __construct(Doctrine_Connection $connection = null, + Doctrine_Hydrator_Abstract $hydrator = null) + { + if ($connection === null) { + $connection = Doctrine_Manager::getInstance()->getCurrentConnection(); + } + if ($hydrator === null) { + $hydrator = new Doctrine_Hydrator(); + } + $this->_conn = $connection; + $this->_hydrator = $hydrator; + $this->_tokenizer = new Doctrine_Query_Tokenizer(); + } + + public function getHydrator() + { + return $this->_hydrator; + } + + /** + * setOption + * + * @param string $name option name + * @param string $value option value + * @return Doctrine_Query this object + */ + public function setOption($name, $value) + { + if ( ! isset($this->_options[$name])) { + throw new Doctrine_Query_Exception('Unknown option ' . $name); + } + $this->_options[$name] = $value; + } + + /** + * hasTableAlias + * whether or not this object has given tableAlias + * + * @param string $tableAlias the table alias to be checked + * @return boolean true if this object has given alias, otherwise false + * @deprecated + */ + public function hasTableAlias($sqlTableAlias) + { + return $this->hasSqlTableAlias($sqlTableAlias); + } + + /** + * hasSqlTableAlias + * whether or not this object has given tableAlias + * + * @param string $tableAlias the table alias to be checked + * @return boolean true if this object has given alias, otherwise false + */ + public function hasSqlTableAlias($sqlTableAlias) + { + return (isset($this->_tableAliasMap[$sqlTableAlias])); + } + + /** + * getTableAliases + * returns all table aliases + * + * @return array table aliases as an array + * @deprecated + */ + public function getTableAliases() + { + return $this->getTableAliasMap(); + } + + /** + * getTableAliasMap + * returns all table aliases + * + * @return array table aliases as an array + */ + public function getTableAliasMap() + { + return $this->_tableAliasMap; + } + + /** + * getQueryPart + * gets a query part from the query part array + * + * @param string $name the name of the query part to be set + * @param string $part query part string + * @throws Doctrine_Query_Exception if trying to set unknown query part + * @return Doctrine_Query_Abstract this object + * @deprecated + */ + public function getQueryPart($part) + { + return $this->getSqlQueryPart($part); + } + + /** + * getSqlQueryPart + * gets an SQL query part from the SQL query part array + * + * @param string $name the name of the query part to be set + * @param string $part query part string + * @throws Doctrine_Query_Exception if trying to set unknown query part + * @return Doctrine_Hydrate this object + */ + public function getSqlQueryPart($part) + { + if ( ! isset($this->_sqlParts[$part])) { + throw new Doctrine_Query_Exception('Unknown SQL query part ' . $part); + } + return $this->_sqlParts[$part]; + } + + /** + * setQueryPart + * sets a query part in the query part array + * + * @param string $name the name of the query part to be set + * @param string $part query part string + * @throws Doctrine_Query_Exception if trying to set unknown query part + * @return Doctrine_Hydrate this object + * @deprecated + */ + public function setQueryPart($name, $part) + { + return $this->setSqlQueryPart($name, $part); + } + + /** + * setSqlQueryPart + * sets an SQL query part in the SQL query part array + * + * @param string $name the name of the query part to be set + * @param string $part query part string + * @throws Doctrine_Query_Exception if trying to set unknown query part + * @return Doctrine_Hydrate this object + */ + public function setSqlQueryPart($name, $part) + { + if ( ! isset($this->_sqlParts[$name])) { + throw new Doctrine_Query_Exception('Unknown query part ' . $name); + } + + if ($name !== 'limit' && $name !== 'offset') { + if (is_array($part)) { + $this->_sqlParts[$name] = $part; + } else { + $this->_sqlParts[$name] = array($part); + } + } else { + $this->_sqlParts[$name] = $part; + } + + return $this; + } + + /** + * addQueryPart + * adds a query part in the query part array + * + * @param string $name the name of the query part to be added + * @param string $part query part string + * @throws Doctrine_Query_Exception if trying to add unknown query part + * @return Doctrine_Hydrate this object + * @deprecated + */ + public function addQueryPart($name, $part) + { + return $this->addSqlQueryPart($name, $part); + } + + /** + * addSqlQueryPart + * adds an SQL query part to the SQL query part array + * + * @param string $name the name of the query part to be added + * @param string $part query part string + * @throws Doctrine_Query_Exception if trying to add unknown query part + * @return Doctrine_Hydrate this object + */ + public function addSqlQueryPart($name, $part) + { + if ( ! isset($this->_sqlParts[$name])) { + throw new Doctrine_Query_Exception('Unknown query part ' . $name); + } + if (is_array($part)) { + $this->_sqlParts[$name] = array_merge($this->_sqlParts[$name], $part); + } else { + $this->_sqlParts[$name][] = $part; + } + return $this; + } + + /** + * removeQueryPart + * removes a query part from the query part array + * + * @param string $name the name of the query part to be removed + * @throws Doctrine_Query_Exception if trying to remove unknown query part + * @return Doctrine_Hydrate this object + * @deprecated + */ + public function removeQueryPart($name) + { + return $this->removeSqlQueryPart($name); + } + + /** + * removeSqlQueryPart + * removes a query part from the query part array + * + * @param string $name the name of the query part to be removed + * @throws Doctrine_Query_Exception if trying to remove unknown query part + * @return Doctrine_Hydrate this object + */ + public function removeSqlQueryPart($name) + { + try { + if ( ! isset($this->_sqlParts[$name])) { + throw new Doctrine_Query_Exception('Unknown query part ' . $name); + }} + catch (Exception $e) {echo $e->getTraceAsString(); echo "


";} + + if ($name == 'limit' || $name == 'offset') { + $this->_sqlParts[$name] = false; + } else { + $this->_sqlParts[$name] = array(); + } + return $this; + } + + /** + * setView + * sets a database view this query object uses + * this method should only be called internally by doctrine + * + * @param Doctrine_View $view database view + * @return void + */ + public function setView(Doctrine_View $view) + { + $this->_view = $view; + } + + /** + * getView + * returns the view associated with this query object (if any) + * + * @return Doctrine_View the view associated with this query object + */ + public function getView() + { + return $this->_view; + } + + /** + * limitSubqueryUsed + * + * @return boolean + */ + public function isLimitSubqueryUsed() + { + return $this->_isLimitSubqueryUsed; + } + + /** + * convertEnums + * convert enum parameters to their integer equivalents + * + * @return array converted parameter array + */ + public function convertEnums($params) + { + foreach ($this->_enumParams as $key => $values) { + if (isset($params[$key])) { + if ( ! empty($values)) { + $params[$key] = $values[0]->enumIndex($values[1], $params[$key]); + } + } + } + return $params; + } + + /** + * Creates the SQL snippet for additional joins. + * + * @return string The created SQL snippet. + */ + protected function _createCustomJoinSql($componentName, $componentAlias) + { + $table = $this->_conn->getMetadata($componentName); + $tableAlias = $this->getSqlTableAlias($componentAlias, $table->getTableName()); + $customJoins = $this->_conn->getMapper($componentName)->getCustomJoins(); + $sql = ''; + foreach ($customJoins as $componentName => $joinType) { + $joinedTable = $this->_conn->getMetadata($componentName); + $joinedAlias = $componentAlias . '.' . $componentName; + $joinedTableAlias = $this->getSqlTableAlias($joinedAlias, $joinedTable->getTableName()); + $sql .= " $joinType JOIN " . $this->_conn->quoteIdentifier($joinedTable->getTableName()) + . ' ' . $this->_conn->quoteIdentifier($joinedTableAlias) . ' ON '; + + foreach ($table->getIdentifierColumnNames() as $column) { + $sql .= $this->_conn->quoteIdentifier($tableAlias) + . '.' . $this->_conn->quoteIdentifier($column) + . ' = ' . $this->_conn->quoteIdentifier($joinedTableAlias) + . '.' . $this->_conn->quoteIdentifier($column); + } + } + + return $sql; + } + + /** + * Creates the SQL snippet for the WHERE part that contains the discriminator + * column conditions. + * Used solely for Single Table Inheritance. + * + * @return string The created SQL snippet. + */ + protected function _createDiscriminatorConditionSql() + { + $array = array(); + foreach ($this->_queryComponents as $componentAlias => $data) { + $sqlTableAlias = $this->getSqlTableAlias($componentAlias); + if ($data['table']->getInheritanceType() != Doctrine::INHERITANCETYPE_SINGLE_TABLE) { + $array[$sqlTableAlias][] = array(); + } else { + $discCol = $data['table']->getInheritanceOption('discriminatorColumn'); + $discMap = $data['table']->getInheritanceOption('discriminatorMap'); + $discValue = array_search($data['table']->getClassName(), $discMap); + if ($discValue === false) { + continue; + } + $discriminator = array(); + $discriminator[] = array($discCol => $discValue); + + $subclasses = $data['table']->getSubclasses(); + foreach ((array)$subclasses as $subclass) { + $subClassMetadata = $this->_conn->getClassMetadata($subclass); + $discCol = $subClassMetadata->getInheritanceOption('discriminatorColumn'); + $discMap = $subClassMetadata->getInheritanceOption('discriminatorMap'); + $discValue = array_search($subclass, $discMap); + $discriminator[] = array($discCol => $discValue); + } + + $array[$sqlTableAlias][] = $discriminator; + } + } + //var_dump($array); + // apply inheritance maps + $str = ''; + $c = array(); + + $index = 0; + foreach ($array as $tableAlias => $maps) { + $a = array(); + + // don't use table aliases if the query isn't a select query + if ($this->_type !== Doctrine_Query::SELECT) { + $tableAlias = ''; + } else { + $tableAlias .= '.'; + } + + foreach ($maps as $map) { + //echo "start"; + $b = array(); + foreach ($map as $discriminator) { + list($column, $value) = each($discriminator); + $identifier = $this->_conn->quoteIdentifier($tableAlias . $column); + if ($index > 0) { + $b[] = '(' . $identifier . ' = ' . $this->_conn->quote($value) + . ' OR ' . $identifier . ' IS NULL)'; + } else { + $b[] = $identifier . ' = ' . $this->_conn->quote($value); + } + } + + if ( ! empty($b)) { + if (count($b) > 1) { + $a[] = '(' . implode(' OR ', $b) . ')'; + } else { + $a[] = implode(' OR ', $b); + } + } + } + //echo "end
"; + if ( ! empty($a)) { + $c[] = implode(' AND ', $a); + } + $index++; + } + + $str .= implode(' AND ', $c); + + return $str; + } + + /** + * getTableAlias + * some database such as Oracle need the identifier lengths to be < ~30 chars + * hence Doctrine creates as short identifier aliases as possible + * + * this method is used for the creation of short table aliases, its also + * smart enough to check if an alias already exists for given component (componentAlias) + * + * @param string $componentAlias the alias for the query component to search table alias for + * @param string $tableName the table name from which the table alias is being created + * @return string the generated / fetched short alias + * @deprecated + */ + public function getTableAlias($componentAlias, $tableName = null) + { + return $this->getSqlTableAlias($componentAlias, $tableName); + } + + /** + * getSqlTableAlias + * some database such as Oracle need the identifier lengths to be < ~30 chars + * hence Doctrine creates as short identifier aliases as possible + * + * this method is used for the creation of short table aliases, its also + * smart enough to check if an alias already exists for given component (componentAlias) + * + * @param string $componentAlias the alias for the query component to search table alias for + * @param string $tableName the table name from which the table alias is being created + * @return string the generated / fetched short alias + */ + public function getSqlTableAlias($componentAlias, $tableName = null) + { + $alias = array_search($componentAlias, $this->_tableAliasMap); + + if ($alias !== false) { + return $alias; + } + + if ($tableName === null) { + throw new Doctrine_Query_Exception("Couldn't get short alias for " . $componentAlias); + } + + return $this->generateTableAlias($componentAlias, $tableName); + } + + /** + * generateNewTableAlias + * generates a new alias from given table alias + * + * @param string $tableAlias table alias from which to generate the new alias from + * @return string the created table alias + * @deprecated + */ + public function generateNewTableAlias($oldAlias) + { + return $this->generateNewSqlTableAlias($oldAlias); + } + + /** + * generateNewSqlTableAlias + * generates a new alias from given table alias + * + * @param string $tableAlias table alias from which to generate the new alias from + * @return string the created table alias + */ + public function generateNewSqlTableAlias($oldAlias) + { + if (isset($this->_tableAliasMap[$oldAlias])) { + // generate a new alias + $name = substr($oldAlias, 0, 1); + $i = ((int) substr($oldAlias, 1)); + + if ($i == 0) { + $i = 1; + } + + $newIndex = ($this->_tableAliasSeeds[$name] + $i); + + return $name . $newIndex; + } + + return $oldAlias; + } + + /** + * getTableAliasSeed + * returns the alias seed for given table alias + * + * @param string $tableAlias table alias that identifies the alias seed + * @return integer table alias seed + * @deprecated + */ + public function getTableAliasSeed($sqlTableAlias) + { + return $this->getSqlTableAliasSeed($sqlTableAlias); + } + + /** + * getSqlTableAliasSeed + * returns the alias seed for given table alias + * + * @param string $tableAlias table alias that identifies the alias seed + * @return integer table alias seed + */ + public function getSqlTableAliasSeed($sqlTableAlias) + { + if ( ! isset($this->_tableAliasSeeds[$sqlTableAlias])) { + return 0; + } + return $this->_tableAliasSeeds[$sqlTableAlias]; + } + + /** + * hasAliasDeclaration + * whether or not this object has a declaration for given component alias + * + * @param string $componentAlias the component alias the retrieve the declaration from + * @return boolean + * @deprecated + */ + public function hasAliasDeclaration($componentAlias) + { + return $this->hasQueryComponent($componentAlias); + } + + /** + * getAliasDeclaration + * get the declaration for given component alias + * + * @param string $componentAlias the component alias the retrieve the declaration from + * @return array the alias declaration + * @deprecated + */ + public function getAliasDeclaration($componentAlias) + { + return $this->getQueryComponent($componentAlias); + } + + /** + * hasQueryComponent + * whether or not this object has a declaration for given component alias + * + * @param string $componentAlias the component alias the retrieve the declaration from + * @return boolean + */ + public function hasQueryComponent($componentAlias) + { + return isset($this->_queryComponents[$componentAlias]); + } + + /** + * getQueryComponent + * get the declaration for given component alias + * + * @param string $componentAlias the component alias the retrieve the declaration from + * @return array the alias declaration + */ + public function getQueryComponent($componentAlias) + { + if ( ! isset($this->_queryComponents[$componentAlias])) { + throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); + } + + return $this->_queryComponents[$componentAlias]; + } + + /** + * Sets the declaration for given component alias + * + * @param string $componentAlias the component alias to set the declaration to + * @param string $queryComponent the alias declaration + */ + public function setQueryComponent($componentAlias, array $queryComponent) + { + $this->_queryComponents[$componentAlias] = $queryComponent; + } + + /** + * copyAliases + * copy aliases from another Hydrate object + * + * this method is needed by DQL subqueries which need the aliases + * of the parent query + * + * @param Doctrine_Hydrate $query the query object from which the + * aliases are copied from + * @return Doctrine_Hydrate this object + */ + public function copyAliases(Doctrine_Query_Abstract $query) + { + $this->_tableAliasMap = $query->_tableAliasMap; + $this->_queryComponents = $query->_queryComponents; + $this->_tableAliasSeeds = $query->_tableAliasSeeds; + return $this; + } + + /** + * getRootAlias + * returns the alias of the the root component + * + * @return array + */ + public function getRootAlias() + { + if ( ! $this->_queryComponents) { + $this->getSql(); + } + reset($this->_queryComponents); + + return key($this->_queryComponents); + } + + /** + * getRootDeclaration + * returns the root declaration + * + * @return array + */ + public function getRootDeclaration() + { + $map = reset($this->_queryComponents); + return $map; + } + + /** + * getRoot + * returns the root component for this object + * + * @return Doctrine_Table root components table + */ + public function getRoot() + { + $map = reset($this->_queryComponents); + + if ( ! isset($map['table'])) { + throw new Doctrine_Query_Exception('Root component not initialized.'); + } + + return $map['table']; + } + + /** + * generateTableAlias + * generates a table alias from given table name and associates + * it with given component alias + * + * @param string $componentAlias the component alias to be associated with generated table alias + * @param string $tableName the table name from which to generate the table alias + * @return string the generated table alias + * @deprecated + */ + public function generateTableAlias($componentAlias, $tableName) + { + return $this->generateSqlTableAlias($componentAlias, $tableName); + } + + /** + * generateSqlTableAlias + * generates a table alias from given table name and associates + * it with given component alias + * + * @param string $componentAlias the component alias to be associated with generated table alias + * @param string $tableName the table name from which to generate the table alias + * @return string the generated table alias + */ + public function generateSqlTableAlias($componentAlias, $tableName) + { + $char = strtolower(substr($tableName, 0, 1)); + + $alias = $char; + + if ( ! isset($this->_tableAliasSeeds[$alias])) { + $this->_tableAliasSeeds[$alias] = 1; + } + + while (isset($this->_tableAliasMap[$alias])) { + if ( ! isset($this->_tableAliasSeeds[$alias])) { + $this->_tableAliasSeeds[$alias] = 1; + } + $alias = $char . ++$this->_tableAliasSeeds[$alias]; + } + + $this->_tableAliasMap[$alias] = $componentAlias; + + return $alias; + } + + /** + * getComponentAlias + * get component alias associated with given table alias + * + * @param string $sqlTableAlias the SQL table alias that identifies the component alias + * @return string component alias + */ + public function getComponentAlias($sqlTableAlias) + { + if ( ! isset($this->_tableAliasMap[$sqlTableAlias])) { + throw new Doctrine_Query_Exception('Unknown table alias ' . $sqlTableAlias); + } + return $this->_tableAliasMap[$sqlTableAlias]; + } + + /** + * _execute + * + * @param array $params + * @return PDOStatement The executed PDOStatement. + */ + protected function _execute($params) + { + $params = $this->_conn->convertBooleans($params); + + if ( ! $this->_view) { + if ($this->_queryCache || $this->_conn->getAttribute(Doctrine::ATTR_QUERY_CACHE)) { + $queryCacheDriver = $this->getQueryCacheDriver(); + // calculate hash for dql query + $dql = $this->getDql(); + $hash = md5($dql . 'DOCTRINE_QUERY_CACHE_SALT'); + $cached = $queryCacheDriver->fetch($hash); + if ($cached) { + $query = $this->_constructQueryFromCache($cached); + } else { + $query = $this->getSqlQuery($params); + $serializedQuery = $this->getCachedForm($query); + $queryCacheDriver->save($hash, $serializedQuery, $this->_queryCacheTTL); + } + } else { + $query = $this->getSqlQuery($params); + } + } else { + $query = $this->_view->getSelectSql(); + } + + $params = $this->convertEnums($params); + + if ($this->isLimitSubqueryUsed() && + $this->_conn->getAttribute(Doctrine::ATTR_DRIVER_NAME) !== 'mysql') { + $params = array_merge($params, $params); + } + + if ($this->_type !== self::SELECT) { + return $this->_conn->exec($query, $params); + } + + $stmt = $this->_conn->execute($query, $params); + + return $stmt; + } + + /** + * execute + * executes the query and populates the data set + * + * @param string $params + * @return Doctrine_Collection the root collection + */ + public function execute($params = array(), $hydrationMode = null) + { + $params = array_merge($this->_params['join'], + $this->_params['set'], + $this->_params['where'], + $this->_params['having'], + $params); + + if ($this->_resultCache) { + $cacheDriver = $this->getResultCacheDriver(); + + $dql = $this->getDql(); + // calculate hash for dql query + $hash = md5($dql . var_export($params, true)); + + $cached = ($this->_expireResultCache) ? false : $cacheDriver->fetch($hash); + + if ($cached === false) { + // cache miss + $stmt = $this->_execute($params); + $this->_hydrator->setQueryComponents($this->_queryComponents); + $result = $this->_hydrator->hydrateResultSet($stmt, $this->_tableAliasMap, + Doctrine::HYDRATE_ARRAY); + + $cached = $this->getCachedForm($result); + $cacheDriver->save($hash, $cached, $this->_resultCacheTTL); + return $result; + } else { + return $this->_constructQueryFromCache($cached); + } + } else { + $stmt = $this->_execute($params); + + if (is_integer($stmt)) { + return $stmt; + } + + $this->_hydrator->setAliasDeclarations($this->_queryComponents); + return $this->_hydrator->hydrateResultSet($stmt, $this->_tableAliasMap, $hydrationMode); + } + } + + /** + * Constructs the query from the cached form. + * + * @param string The cached query, in a serialized form. + * @return array The custom component that was cached together with the essential + * query data. This can be either a result set (result caching) + * or an SQL query string (query caching). + */ + protected function _constructQueryFromCache($cached) + { + $cached = unserialize($cached); + $this->_tableAliasMap = $cached[2]; + $customComponent = $cached[0]; + + $queryComponents = array(); + $cachedComponents = $cached[1]; + foreach ($cachedComponents as $alias => $components) { + $e = explode('.', $components[0]); + if (count($e) === 1) { + $queryComponents[$alias]['mapper'] = $this->_conn->getMapper($e[0]); + $queryComponents[$alias]['table'] = $queryComponents[$alias]['mapper']->getTable(); + } else { + $queryComponents[$alias]['parent'] = $e[0]; + $queryComponents[$alias]['relation'] = $queryComponents[$e[0]]['table']->getRelation($e[1]); + $queryComponents[$alias]['mapper'] = $this->_conn->getMapper($queryComponents[$alias]['relation']->getForeignComponentName()); + $queryComponents[$alias]['table'] = $queryComponents[$alias]['mapper']->getTable(); + } + if (isset($v[1])) { + $queryComponents[$alias]['agg'] = $components[1]; + } + if (isset($v[2])) { + $queryComponents[$alias]['map'] = $components[2]; + } + } + $this->_queryComponents = $queryComponents; + + return $customComponent; + } + + /** + * getCachedForm + * returns the cached form of this query for given resultSet + * + * @param array $resultSet + * @return string serialized string representation of this query + */ + public function getCachedForm($customComponent = null) + { + $componentInfo = array(); + + foreach ($this->getQueryComponents() as $alias => $components) { + if ( ! isset($components['parent'])) { + $componentInfo[$alias][] = $components['mapper']->getComponentName(); + //$componentInfo[$alias][] = $components['mapper']->getComponentName(); + } else { + $componentInfo[$alias][] = $components['parent'] . '.' . $components['relation']->getAlias(); + } + if (isset($components['agg'])) { + $componentInfo[$alias][] = $components['agg']; + } + if (isset($components['map'])) { + $componentInfo[$alias][] = $components['map']; + } + } + + return serialize(array($customComponent, $componentInfo, $this->getTableAliasMap())); + } + + /** + * addSelect + * adds fields to the SELECT part of the query + * + * @param string $select Query SELECT part + * @return Doctrine_Query + */ + public function addSelect($select) + { + return $this->_addDqlQueryPart('select', $select, true); + } + + /** + * addTableAlias + * adds an alias for table and associates it with given component alias + * + * @param string $componentAlias the alias for the query component associated with given tableAlias + * @param string $tableAlias the table alias to be added + * @return Doctrine_Hydrate + * @deprecated + */ + public function addTableAlias($tableAlias, $componentAlias) + { + return $this->addSqlTableAlias($tableAlias, $componentAlias); + } + + /** + * addSqlTableAlias + * adds an SQL table alias and associates it a component alias + * + * @param string $componentAlias the alias for the query component associated with given tableAlias + * @param string $tableAlias the table alias to be added + * @return Doctrine_Query_Abstract + */ + public function addSqlTableAlias($sqlTableAlias, $componentAlias) + { + $this->_tableAliasMap[$sqlTableAlias] = $componentAlias; + return $this; + } + + /** + * addFrom + * adds fields to the FROM part of the query + * + * @param string $from Query FROM part + * @return Doctrine_Query + */ + public function addFrom($from) + { + return $this->_addDqlQueryPart('from', $from, true); + } + + /** + * addWhere + * adds conditions to the WHERE part of the query + * + * @param string $where Query WHERE part + * @param mixed $params an array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function addWhere($where, $params = array()) + { + if (is_array($params)) { + $this->_params['where'] = array_merge($this->_params['where'], $params); + } else { + $this->_params['where'][] = $params; + } + return $this->_addDqlQueryPart('where', $where, true); + } + + /** + * whereIn + * adds IN condition to the query WHERE part + * + * @param string $expr the operand of the IN + * @param mixed $params an array of parameters or a simple scalar + * @param boolean $not whether or not to use NOT in front of IN + * @return Doctrine_Query + */ + public function whereIn($expr, $params = array(), $not = false) + { + $params = (array) $params; + + // if there's no params, return (else we'll get a WHERE IN (), invalid SQL) + if (!count($params)) + return $this; + + $a = array(); + foreach ($params as $k => $value) { + if ($value instanceof Doctrine_Expression) { + $value = $value->getSql(); + unset($params[$k]); + } else { + $value = '?'; + } + $a[] = $value; + } + + $this->_params['where'] = array_merge($this->_params['where'], $params); + + $where = $expr . ($not === true ? ' NOT ':'') . ' IN (' . implode(', ', $a) . ')'; + + return $this->_addDqlQueryPart('where', $where, true); + } + + /** + * whereNotIn + * adds NOT IN condition to the query WHERE part + * + * @param string $expr the operand of the NOT IN + * @param mixed $params an array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function whereNotIn($expr, $params = array()) + { + return $this->whereIn($expr, $params, true); + } + + /** + * addGroupBy + * adds fields to the GROUP BY part of the query + * + * @param string $groupby Query GROUP BY part + * @return Doctrine_Query + */ + public function addGroupBy($groupby) + { + return $this->_addDqlQueryPart('groupby', $groupby, true); + } + + /** + * addHaving + * adds conditions to the HAVING part of the query + * + * @param string $having Query HAVING part + * @param mixed $params an array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function addHaving($having, $params = array()) + { + if (is_array($params)) { + $this->_params['having'] = array_merge($this->_params['having'], $params); + } else { + $this->_params['having'][] = $params; + } + return $this->_addDqlQueryPart('having', $having, true); + } + + /** + * addOrderBy + * adds fields to the ORDER BY part of the query + * + * @param string $orderby Query ORDER BY part + * @return Doctrine_Query + */ + public function addOrderBy($orderby) + { + return $this->_addDqlQueryPart('orderby', $orderby, true); + } + + /** + * select + * sets the SELECT part of the query + * + * @param string $select Query SELECT part + * @return Doctrine_Query + */ + public function select($select) + { + return $this->_addDqlQueryPart('select', $select); + } + + /** + * distinct + * Makes the query SELECT DISTINCT. + * + * @param bool $flag Whether or not the SELECT is DISTINCT (default true). + * @return Doctrine_Query + */ + public function distinct($flag = true) + { + $this->_sqlParts['distinct'] = (bool) $flag; + return $this; + } + + /** + * forUpdate + * Makes the query SELECT FOR UPDATE. + * + * @param bool $flag Whether or not the SELECT is FOR UPDATE (default true). + * @return Doctrine_Query + */ + public function forUpdate($flag = true) + { + $this->_sqlParts[self::FOR_UPDATE] = (bool) $flag; + return $this; + } + + /** + * delete + * sets the query type to DELETE + * + * @return Doctrine_Query + */ + public function delete() + { + $this->_type = self::DELETE; + return $this; + } + + /** + * update + * sets the UPDATE part of the query + * + * @param string $update Query UPDATE part + * @return Doctrine_Query + */ + public function update($update) + { + $this->_type = self::UPDATE; + return $this->_addDqlQueryPart('from', $update); + } + + /** + * set + * sets the SET part of the query + * + * @param string $update Query UPDATE part + * @return Doctrine_Query + */ + public function set($key, $value, $params = null) + { + if (is_array($key)) { + foreach ($key as $k => $v) { + $this->set($k, '?', array($v)); + } + return $this; + } else { + if ($params !== null) { + if (is_array($params)) { + $this->_params['set'] = array_merge($this->_params['set'], $params); + } else { + $this->_params['set'][] = $params; + } + } + return $this->_addDqlQueryPart('set', $key . ' = ' . $value, true); + } + } + + /** + * from + * sets the FROM part of the query + * + * @param string $from Query FROM part + * @return Doctrine_Query + */ + public function from($from) + { + return $this->_addDqlQueryPart('from', $from); + } + + /** + * innerJoin + * appends an INNER JOIN to the FROM part of the query + * + * @param string $join Query INNER JOIN + * @return Doctrine_Query + */ + public function innerJoin($join, $params = array()) + { + if (is_array($params)) { + $this->_params['join'] = array_merge($this->_params['join'], $params); + } else { + $this->_params['join'][] = $params; + } + + return $this->_addDqlQueryPart('from', 'INNER JOIN ' . $join, true); + } + + /** + * leftJoin + * appends a LEFT JOIN to the FROM part of the query + * + * @param string $join Query LEFT JOIN + * @return Doctrine_Query + */ + public function leftJoin($join, $params = array()) + { + if (is_array($params)) { + $this->_params['join'] = array_merge($this->_params['join'], $params); + } else { + $this->_params['join'][] = $params; + } + + return $this->_addDqlQueryPart('from', 'LEFT JOIN ' . $join, true); + } + + /** + * groupBy + * sets the GROUP BY part of the query + * + * @param string $groupby Query GROUP BY part + * @return Doctrine_Query + */ + public function groupBy($groupby) + { + return $this->_addDqlQueryPart('groupby', $groupby); + } + + /** + * where + * sets the WHERE part of the query + * + * @param string $join Query WHERE part + * @param mixed $params an array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function where($where, $params = array()) + { + $this->_params['where'] = array(); + if (is_array($params)) { + $this->_params['where'] = $params; + } else { + $this->_params['where'][] = $params; + } + + return $this->_addDqlQueryPart('where', $where); + } + + /** + * having + * sets the HAVING part of the query + * + * @param string $having Query HAVING part + * @param mixed $params an array of parameters or a simple scalar + * @return Doctrine_Query + */ + public function having($having, $params = array()) + { + $this->_params['having'] = array(); + if (is_array($params)) { + $this->_params['having'] = $params; + } else { + $this->_params['having'][] = $params; + } + + return $this->_addDqlQueryPart('having', $having); + } + + /** + * orderBy + * sets the ORDER BY part of the query + * + * @param string $orderby Query ORDER BY part + * @return Doctrine_Query + */ + public function orderBy($orderby) + { + return $this->_addDqlQueryPart('orderby', $orderby); + } + + /** + * limit + * sets the Query query limit + * + * @param integer $limit limit to be used for limiting the query results + * @return Doctrine_Query + */ + public function limit($limit) + { + return $this->_addDqlQueryPart('limit', $limit); + } + + /** + * offset + * sets the Query query offset + * + * @param integer $offset offset to be used for paginating the query + * @return Doctrine_Query + */ + public function offset($offset) + { + return $this->_addDqlQueryPart('offset', $offset); + } + + /** + * getSql + * shortcut for {@link getSqlQuery()}. + * + * @return string sql query string + */ + public function getSql() + { + return $this->getSqlQuery(); + } + + /** + * clear + * resets all the variables + * + * @return void + */ + protected function clear() + { + $this->_sqlParts = array( + 'select' => array(), + 'distinct' => false, + 'forUpdate' => false, + 'from' => array(), + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => false, + 'offset' => false, + ); + } + + public function setHydrationMode($hydrationMode) + { + $this->_hydrator->setHydrationMode($hydrationMode); + return $this; + } + + /** + * @deprecated + */ + public function getAliasMap() + { + return $this->_queryComponents; + } + + /** + * Gets the components of this query. + */ + public function getQueryComponents() + { + return $this->_queryComponents; + } + + /** + * Return the SQL parts. + * + * @return array The parts + * @deprecated + */ + public function getParts() + { + return $this->getSqlParts(); + } + + /** + * Return the SQL parts. + * + * @return array The parts + */ + public function getSqlParts() + { + return $this->_sqlParts; + } + + /** + * getType + * + * returns the type of this query object + * by default the type is Doctrine_Query_Abstract::SELECT but if update() or delete() + * are being called the type is Doctrine_Query_Abstract::UPDATE and Doctrine_Query_Abstract::DELETE, + * respectively + * + * @see Doctrine_Query_Abstract::SELECT + * @see Doctrine_Query_Abstract::UPDATE + * @see Doctrine_Query_Abstract::DELETE + * + * @return integer return the query type + */ + public function getType() + { + return $this->_type; + } + + /** + * useCache + * + * @param Doctrine_Cache_Interface|bool $driver cache driver + * @param integer $timeToLive how long the cache entry is valid + * @return Doctrine_Hydrate this object + * @deprecated Use useResultCache() + */ + public function useCache($driver = true, $timeToLive = null) + { + return $this->useResultCache($driver, $timeToLive); + } + + /** + * useResultCache + * + * @param Doctrine_Cache_Interface|bool $driver cache driver + * @param integer $timeToLive how long the cache entry is valid + * @return Doctrine_Hydrate this object + */ + public function useResultCache($driver = true, $timeToLive = null) + { + if ($driver !== null && $driver !== true && ! ($driver instanceOf Doctrine_Cache_Interface)){ + $msg = 'First argument should be instance of Doctrine_Cache_Interface or null.'; + throw new Doctrine_Query_Exception($msg); + } + $this->_resultCache = $driver; + + return $this->setResultCacheLifeSpan($timeToLive); + } + + /** + * useQueryCache + * + * @param Doctrine_Cache_Interface|bool $driver cache driver + * @param integer $timeToLive how long the cache entry is valid + * @return Doctrine_Hydrate this object + */ + public function useQueryCache(Doctrine_Cache_Interface $driver, $timeToLive = null) + { + $this->_queryCache = $driver; + return $this->setQueryCacheLifeSpan($timeToLive); + } + + /** + * expireCache + * + * @param boolean $expire whether or not to force cache expiration + * @return Doctrine_Hydrate this object + * @deprecated Use expireResultCache() + */ + public function expireCache($expire = true) + { + return $this->expireResultCache($expire); + } + + /** + * expireCache + * + * @param boolean $expire whether or not to force cache expiration + * @return Doctrine_Hydrate this object + */ + public function expireResultCache($expire = true) + { + $this->_expireResultCache = true; + return $this; + } + + /** + * expireQueryCache + * + * @param boolean $expire whether or not to force cache expiration + * @return Doctrine_Hydrate this object + */ + public function expireQueryCache($expire = true) + { + $this->_expireQueryCache = true; + return $this; + } + + /** + * setCacheLifeSpan + * + * @param integer $timeToLive how long the cache entry is valid + * @return Doctrine_Hydrate this object + * @deprecated Use setResultCacheLifeSpan() + */ + public function setCacheLifeSpan($timeToLive) + { + return $this->setResultCacheLifeSpan($timeToLive); + } + + /** + * setResultCacheLifeSpan + * + * @param integer $timeToLive how long the cache entry is valid + * @return Doctrine_Hydrate this object + */ + public function setResultCacheLifeSpan($timeToLive) + { + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; + } + $this->_resultCacheTTL = $timeToLive; + + return $this; + } + + /** + * setQueryCacheLifeSpan + * + * @param integer $timeToLive how long the cache entry is valid + * @return Doctrine_Hydrate this object + */ + public function setQueryCacheLifeSpan($timeToLive) + { + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; + } + $this->_queryCacheTTL = $timeToLive; + + return $this; + } + + /** + * getCacheDriver + * returns the cache driver associated with this object + * + * @return Doctrine_Cache_Interface|boolean|null cache driver + * @deprecated Use getResultCacheDriver() + */ + public function getCacheDriver() + { + return $this->getResultCacheDriver(); + } + + /** + * getResultCacheDriver + * returns the cache driver used for caching result sets + * + * @return Doctrine_Cache_Interface|boolean|null cache driver + */ + public function getResultCacheDriver() + { + if ($this->_resultCache instanceof Doctrine_Cache_Interface) { + return $this->_resultCache; + } else { + return $this->_conn->getResultCacheDriver(); + } + } + + /** + * getQueryCacheDriver + * returns the cache driver used for caching queries + * + * @return Doctrine_Cache_Interface|boolean|null cache driver + */ + public function getQueryCacheDriver() + { + if ($this->_queryCache instanceof Doctrine_Cache_Interface) { + return $this->_queryCache; + } else { + return $this->_conn->getQueryCacheDriver(); + } + } + + /** + * getConnection + * + * @return Doctrine_Connection + */ + public function getConnection() + { + return $this->_conn; + } + + public function setConnection(Doctrine_Connection $conn) + { + $this->_conn = $conn; + } + + /** + * Adds a DQL part to the internal parts collection. + * + * @param string $queryPartName The name of the query part. + * @param string $queryPart The actual query part to add. + * @param boolean $append Whether to append $queryPart to already existing + * parts under the same $queryPartName. Defaults to FALSE + * (previously added parts with the same name get overridden). + */ + protected function _addDqlQueryPart($queryPartName, $queryPart, $append = false) + { + if ($append) { + $this->_dqlParts[$queryPartName][] = $queryPart; + } else { + $this->_dqlParts[$queryPartName] = array($queryPart); + } + + $this->_state = Doctrine_Query::STATE_DIRTY; + return $this; + } + + /** + * Gets the SQL query that corresponds to this query object. + * The returned SQL syntax depends on the connection driver that is used + * by this query object at the time of this method call. + * + * @param array $params + */ + abstract public function getSqlQuery($params = array()); + + /** + * @deprecated + */ + public function getQuery($params = array()) + { + return $this->getSqlQuery($params); + } +} diff --git a/lib/Doctrine/Query/CacheHandler.php b/lib/Doctrine/Query/CacheHandler.php new file mode 100755 index 000000000..1e0e7bc2e --- /dev/null +++ b/lib/Doctrine/Query/CacheHandler.php @@ -0,0 +1,156 @@ +. + */ + +Doctrine::autoload('Doctrine_Query_AbstractResult'); +Doctrine::autoload('Doctrine_Query_ParserResult'); +Doctrine::autoload('Doctrine_Query_QueryResult'); + +/** + * Doctrine_Query_CacheHandler + * + * @package Doctrine + * @subpackage Query + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision: 1393 $ + * @author Guilherme Blanco + * @author Konsta Vesterinen + * + * @todo Re-document this class + */ +abstract class Doctrine_Query_CacheHandler +{ + /** + * Static factory method. Receives a Doctrine_Query object and generates + * the object after processing queryComponents. Table aliases are retrieved + * directly from Doctrine_Query_Parser. + * + * @param mixed $result Data to be stored. + * @param Doctrine_Query_ParserResult $parserResult Parser results that enables to have important data retrieved. + */ + public static function fromResultSet($result, $parserResult) + { + $queryComponents = array(); + + foreach ($parserResult->getQueryComponents() as $alias => $components) { + if ( ! isset($components['parent'])) { + $queryComponents[$alias][] = $components['mapper']->getComponentName(); + //$queryComponents[$alias][] = $components['mapper']->getComponentName(); + } else { + $queryComponents[$alias][] = $components['parent'] . '.' . $components['relation']->getAlias(); + } + + if (isset($components['agg'])) { + $queryComponents[$alias][] = $components['agg']; + } + + if (isset($components['map'])) { + $queryComponents[$alias][] = $components['map']; + } + } + + return new Doctrine_Query_QueryResult( + $result, + $queryComponents, + $parserResult->getTableAliasMap(), + $parserResult->getEnumParams() + ); + } + + + + /** + * Static factory method. Receives a Doctrine_Query object and a cached data. + * It handles the cache and generates the object after processing queryComponents. + * Table aliases are retrieved from cache. + * + * @param Doctrine_Query $query Doctrine_Query_Object related to this cache item. + * @param mixed $cached Cached data. + */ + public static function fromCachedResult($query, $cached = false) + { + $cached = unserialize($cached); + + return new Doctrine_Query_QueryResult( + $cached[0], + self::_getQueryComponents($cached[1]), + $cached[2], + $cached[3] + ); + } + + + /** + * Static factory method. Receives a Doctrine_Query object and a cached data. + * It handles the cache and generates the object after processing queryComponents. + * Table aliases are retrieved from cache. + * + * @param Doctrine_Query $query Doctrine_Query_Object related to this cache item. + * @param mixed $cached Cached data. + */ + public static function fromCachedQuery($query, $cached = false) + { + $cached = unserialize($cached); + + return new Doctrine_Query_ParserResult( + $cached[0], + self::_getQueryComponents($cached[1]), + $cached[2], + $cached[3] + ); + } + + + /** + * @nodoc + */ + protected static function _getQueryComponents($query, $cachedQueryComponents) + { + $queryComponents = array(); + + foreach ($cachedQueryComponents as $alias => $components) { + $e = explode('.', $components[0]); + + if (count($e) === 1) { + $queryComponents[$alias]['mapper'] = $query->getConnection()->getMapper($e[0]); + $queryComponents[$alias]['table'] = $queryComponents[$alias]['mapper']->getTable(); + } else { + $queryComponents[$alias]['parent'] = $e[0]; + $queryComponents[$alias]['relation'] = $queryComponents[$e[0]]['table']->getRelation($e[1]); + $queryComponents[$alias]['mapper'] = $query->getConnection()->getMapper($queryComponents[$alias]['relation']->getForeignComponentName()); + $queryComponents[$alias]['table'] = $queryComponents[$alias]['mapper']->getTable(); + } + + if (isset($v[1])) { + $queryComponents[$alias]['agg'] = $components[1]; + } + + if (isset($v[2])) { + $queryComponents[$alias]['map'] = $components[2]; + } + } + + return $queryComponents; + } + +} \ No newline at end of file diff --git a/lib/Doctrine/Query/Check.php b/lib/Doctrine/Query/Check.php deleted file mode 100644 index c9bed06bc..000000000 --- a/lib/Doctrine/Query/Check.php +++ /dev/null @@ -1,169 +0,0 @@ -. - */ - -/** - * Doctrine_Query_Check - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision: 1080 $ - * @author Konsta Vesterinen - */ -class Doctrine_Query_Check -{ - /** - * @var Doctrine_Table $table Doctrine_Table object - */ - protected $table; - - /** - * @var string $sql database specific sql CHECK constraint definition - * parsed from the given dql CHECK definition - */ - protected $sql; - - protected $_tokenizer; - - /** - * @param Doctrine_Table|string $table Doctrine_Table object - */ - public function __construct($table) - { - if ( ! ($table instanceof Doctrine_Table)) { - $table = Doctrine_Manager::getInstance() - ->getCurrentConnection() - ->getClassMetadata($table); - } - $this->table = $table; - $this->_tokenizer = new Doctrine_Query_Tokenizer(); - } - - /** - * getTable - * returns the table object associated with this object - * - * @return Doctrine_Connection - */ - public function getTable() - { - return $this->table; - } - - /** - * parse - * - * @param string $dql DQL CHECK constraint definition - * @return string - */ - public function parse($dql) - { - $this->sql = $this->parseClause($dql); - } - - /** - * parseClause - * - * @param string $alias component alias - * @param string $field the field name - * @param mixed $value the value of the field - * @return void - */ - public function parseClause($dql) - { - $parts = $this->_tokenizer->sqlExplode($dql, ' AND '); - - if (count($parts) > 1) { - $ret = array(); - foreach ($parts as $part) { - $ret[] = $this->parseSingle($part); - } - - $r = implode(' AND ', $ret); - } else { - $parts = $this->_tokenizer->quoteExplode($dql, ' OR '); - if (count($parts) > 1) { - $ret = array(); - foreach ($parts as $part) { - $ret[] = $this->parseClause($part); - } - - $r = implode(' OR ', $ret); - } else { - $ret = $this->parseSingle($dql); - return $ret; - } - } - return '(' . $r . ')'; - } - - public function parseSingle($part) - { - $e = explode(' ', $part); - - $e[0] = $this->parseFunction($e[0]); - - switch ($e[1]) { - case '>': - case '<': - case '=': - case '!=': - case '<>': - - break; - default: - throw new Doctrine_Query_Exception('Unknown operator ' . $e[1]); - } - - return implode(' ', $e); - } - public function parseFunction($dql) - { - if (($pos = strpos($dql, '(')) !== false) { - $func = substr($dql, 0, $pos); - $value = substr($dql, ($pos + 1), -1); - - $expr = $this->table->getConnection()->expression; - - if ( ! method_exists($expr, $func)) { - throw new Doctrine_Query_Exception('Unknown function ' . $func); - } - - $func = $expr->$func($value); - } - return $func; - } - - /** - * getSql - * - * returns database specific sql CHECK constraint definition - * parsed from the given dql CHECK definition - * - * @return string - */ - public function getSql() - { - return $this->sql; - } -} \ No newline at end of file diff --git a/lib/Doctrine/Query/Condition.php b/lib/Doctrine/Query/Condition.php deleted file mode 100644 index 7fc74566a..000000000 --- a/lib/Doctrine/Query/Condition.php +++ /dev/null @@ -1,118 +0,0 @@ -. - */ -Doctrine::autoload('Doctrine_Query_Part'); -/** - * Doctrine_Query_Condition - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - * @author Konsta Vesterinen - */ -abstract class Doctrine_Query_Condition extends Doctrine_Query_Part -{ - /** - * DQL CONDITION PARSER - * parses the join condition/where/having part of the query string - * - * @param string $str - * @return string - */ - public function parse($str) - { - $tmp = trim($str); - - $parts = $this->_tokenizer->bracketExplode($str, array(' \&\& ', ' AND '), '(', ')'); - - if (count($parts) > 1) { - $ret = array(); - foreach ($parts as $part) { - $part = $this->_tokenizer->bracketTrim($part, '(', ')'); - $ret[] = $this->parse($part); - } - $r = implode(' AND ', $ret); - } else { - - $parts = $this->_tokenizer->bracketExplode($str, array(' \|\| ', ' OR '), '(', ')'); - if (count($parts) > 1) { - $ret = array(); - foreach ($parts as $part) { - $part = $this->_tokenizer->bracketTrim($part, '(', ')'); - $ret[] = $this->parse($part); - } - $r = implode(' OR ', $ret); - } else { - // Fix for #710 - if (substr($parts[0],0,1) == '(' && substr($parts[0], -1) == ')') { - return $this->parse(substr($parts[0], 1, -1)); - } else { - // Processing NOT here - if (strtoupper(substr($parts[0], 0, 4)) === 'NOT ') { - $r = 'NOT ('.$this->parse(substr($parts[0], 4)).')'; - } else { - return $this->load($parts[0]); - } - } - } - } - - return '(' . $r . ')'; - } - - /** - * parses a literal value and returns the parsed value - * - * boolean literals are parsed to integers - * components are parsed to associated table aliases - * - * @param string $value literal value to be parsed - * @return string - */ - public function parseLiteralValue($value) - { - // check that value isn't a string - if (strpos($value, '\'') === false) { - // parse booleans - $value = $this->query->getConnection() - ->dataDict->parseBoolean($value); - - $a = explode('.', $value); - - if (count($a) > 1) { - // either a float or a component.. - - if ( ! is_numeric($a[0])) { - // a component found - $field = array_pop($a); - $reference = implode('.', $a); - $value = $this->query->getTableAlias($reference). '.' . $field; - } - } - } else { - // string literal found - } - - return $value; - } -} \ No newline at end of file diff --git a/lib/Doctrine/Query/From.php b/lib/Doctrine/Query/From.php deleted file mode 100644 index 304a5b326..000000000 --- a/lib/Doctrine/Query/From.php +++ /dev/null @@ -1,88 +0,0 @@ -. - */ -Doctrine::autoload("Doctrine_Query_Part"); -/** - * Doctrine_Query_From - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - * @author Konsta Vesterinen - */ -class Doctrine_Query_From extends Doctrine_Query_Part -{ - /** - * DQL FROM PARSER - * parses the FROM part of the query string - * - * @param string $str - * @return void - */ - public function parse($str) - { - $str = trim($str); - $parts = $this->_tokenizer->bracketExplode($str, 'JOIN'); - - $operator = false; - - switch (trim($parts[0])) { - case 'INNER': - $operator = ':'; - case 'LEFT': - array_shift($parts); - break; - } - - $last = ''; - - foreach ($parts as $k => $part) { - $part = trim($part); - - if (empty($part)) { - continue; - } - - $e = explode(' ', $part); - - if (end($e) == 'INNER' || end($e) == 'LEFT') { - $last = array_pop($e); - } - $part = implode(' ', $e); - - foreach ($this->_tokenizer->bracketExplode($part, ',') as $reference) { - $reference = trim($reference); - $e = explode(' ', $reference); - $e2 = explode('.', $e[0]); - - if ($operator) { - $e[0] = array_shift($e2) . $operator . implode('.', $e2); - } - $table = $this->query->load(implode(' ', $e)); - } - - $operator = ($last == 'INNER') ? ':' : '.'; - } - return null; - } -} \ No newline at end of file diff --git a/lib/Doctrine/Query/Having.php b/lib/Doctrine/Query/Having.php deleted file mode 100644 index 28ea094bc..000000000 --- a/lib/Doctrine/Query/Having.php +++ /dev/null @@ -1,102 +0,0 @@ -. - */ -Doctrine::autoload('Doctrine_Query_Condition'); -/** - * Doctrine_Query_Having - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - * @author Konsta Vesterinen - */ -class Doctrine_Query_Having extends Doctrine_Query_Condition -{ - /** - * DQL Aggregate Function parser - * - * @param string $func - * @return mixed - */ - private function parseAggregateFunction($func) - { - $pos = strpos($func, '('); - - if ($pos !== false) { - $funcs = array(); - - $name = substr($func, 0, $pos); - $func = substr($func, ($pos + 1), -1); - $params = $this->_tokenizer->bracketExplode($func, ',', '(', ')'); - - foreach ($params as $k => $param) { - $params[$k] = $this->parseAggregateFunction($param); - } - - $funcs = $name . '(' . implode(', ', $params) . ')'; - - return $funcs; - - } else { - if ( ! is_numeric($func)) { - $a = explode('.', $func); - - if (count($a) > 1) { - $field = array_pop($a); - $reference = implode('.', $a); - $map = $this->query->load($reference, false); - $field = $map['table']->getColumnName($field); - $func = $this->query->getTableAlias($reference) . '.' . $field; - } else { - $field = end($a); - $func = $this->query->getAggregateAlias($field); - } - return $func; - } else { - return $func; - } - } - } - - - /** - * load - * returns the parsed query part - * - * @param string $having - * @return string - */ - final public function load($having) - { - $tokens = $this->_tokenizer->bracketExplode($having, ' ', '(', ')'); - $part = $this->parseAggregateFunction(array_shift($tokens)); - $operator = array_shift($tokens); - $value = implode(' ', $tokens); - $part .= ' ' . $operator . ' ' . $value; - // check the RHS for aggregate functions - if (strpos($value, '(') !== false) { - $value = $this->parseAggregateFunction($value); - } - return $part; - } -} diff --git a/lib/Doctrine/Query/JoinCondition.php b/lib/Doctrine/Query/JoinCondition.php deleted file mode 100644 index 40153a9f1..000000000 --- a/lib/Doctrine/Query/JoinCondition.php +++ /dev/null @@ -1,114 +0,0 @@ -. - */ -Doctrine::autoload('Doctrine_Query_Part'); -/** - * Doctrine_Query_JoinCondition - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - * @author Konsta Vesterinen - */ -class Doctrine_Query_JoinCondition extends Doctrine_Query_Condition -{ - public function load($condition) - { - $condition = trim($condition); - - $e = $this->_tokenizer->sqlExplode($condition); - - if (count($e) > 2) { - $a = explode('.', $e[0]); - $field = array_pop($a); - $reference = implode('.', $a); - $operator = $e[1]; - $value = $e[2]; - - $conn = $this->query->getConnection(); - $alias = $this->query->getTableAlias($reference); - $map = $this->query->getAliasDeclaration($reference); - $table = $map['table']; - // check if value is enumerated value - $enumIndex = $table->enumIndex($field, trim($value, "'")); - - if (false !== $enumIndex && $conn->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)) { - $enumIndex = $conn->quote($enumIndex, 'text'); - } - - if (substr($value, 0, 1) == '(') { - // trim brackets - $trimmed = $this->_tokenizer->bracketTrim($value); - - if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { - // subquery found - $q = $this->query->createSubquery(); - $value = '(' . $q->parseQuery($trimmed)->getQuery() . ')'; - } elseif (substr($trimmed, 0, 4) == 'SQL:') { - $value = '(' . substr($trimmed, 4) . ')'; - } else { - // simple in expression found - $e = $this->_tokenizer->sqlExplode($trimmed, ','); - - $value = array(); - foreach ($e as $part) { - $index = $table->enumIndex($field, trim($part, "'")); - - if (false !== $index && $conn->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)) { - $index = $conn->quote($index, 'text'); - } - - if ($index !== false) { - $value[] = $index; - } else { - $value[] = $this->parseLiteralValue($part); - } - } - $value = '(' . implode(', ', $value) . ')'; - } - } else { - if ($enumIndex !== false) { - $value = $enumIndex; - } else { - $value = $this->parseLiteralValue($value); - } - } - - switch ($operator) { - case '<': - case '>': - case '=': - case '!=': - if ($enumIndex !== false) { - $value = $enumIndex; - } - default: - $condition = $alias . '.' . $field . ' ' - . $operator . ' ' . $value; - } - - } - - return $condition; - } -} diff --git a/lib/Doctrine/Query/Parser.php b/lib/Doctrine/Query/Parser.php index d6eee71e4..220cbdafa 100644 --- a/lib/Doctrine/Query/Parser.php +++ b/lib/Doctrine/Query/Parser.php @@ -1,35 +1,323 @@ -. - */ - -/** - * Doctrine_Query_Parser - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - */ -class Doctrine_Query_Parser -{ - -} \ No newline at end of file +. + */ + +/** + * An LL(k) parser for the context-free grammar of Doctrine Query Language. + * Parses a DQL query, reports any errors in it, and generates the corresponding + * SQL. + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Parser +{ + /** + * The minimum number of tokens read after last detected error before + * another error can be reported. + * + * @var int + */ + const MIN_ERROR_DISTANCE = 2; + + + /** + * The Sql Builder object. + * + * @var Doctrine_Query_SqlBuilder + */ + protected $_sqlbuilder; + + /** + * A scanner object. + * + * @var Doctrine_Query_Scanner + */ + protected $_scanner; + + /** + * The Parser Result object. + * + * @var Doctrine_Query_ParserResult + */ + protected $_parserResult; + + /** + * Keyword symbol table + * + * @var Doctrine_Query_Token + */ + protected $_keywordTable; + + // Scanner Stuff + + /** + * @var array The next token in the query string. + */ + public $lookahead; + + /** + * @var array The last matched token. + */ + public $token; + + // End of Scanner Stuff + + + // Error management stuff + + /** + * Array containing errors detected in the query string during parsing process. + * + * @var array + */ + protected $_errors; + + /** + * @var int The number of tokens read since last error in the input string. + */ + protected $_errorDistance; + + // End of Error management stuff + + + /** + * Creates a new query parser object. + * + * @param string $dql DQL to be parsed. + * @param Doctrine_Connection $connection The connection to use + */ + public function __construct($dql, Doctrine_Connection $connection = null) + { + $this->_scanner = new Doctrine_Query_Scanner($dql); + $this->_parserResult = new Doctrine_Query_ParserResult(); + $this->_sqlBuilder = Doctrine_Query_SqlBuilder::fromConnection($connection); + $this->_keywordTable = new Doctrine_Query_Token(); + + $this->free(true); + } + + + /** + * Attempts to match the given token with the current lookahead token. + * + * If they match, updates the lookahead token; otherwise raises a syntax + * error. + * + * @param int|string token type or value + * @return bool True, if tokens match; false otherwise. + */ + public function match($token) + { + if (is_string($token)) { + $isMatch = ($this->lookahead['value'] === $token); + } else { + $isMatch = ($this->lookahead['type'] === $token); + } + + + if ( ! $isMatch) { + // No definition for value checking. + $this->syntaxError($this->_keywordTable->getLiteral($token)); + + } + + $this->next(); + return true; + } + + + /** + * @todo [TODO] Document these! + */ + public function next() + { + $this->token = $this->lookahead; + $this->lookahead = $this->_scanner->next(); + $this->_errorDistance++; + } + + + public function isA($value, $token) + { + return $this->_scanner->isA($value, $token); + } + + + /** + * Free this parser enabling it to be reused + * + * @param boolean $deep Whether to clean peek and reset errors + * @param integer $position Position to reset + * @return void + */ + public function free($deep = false, $position = 0) + { + // WARNING! Use this method with care. It resets the scanner! + $this->_scanner->resetPosition($position); + + // Deep = true cleans peek and also any previously defined errors + if ($deep) { + $this->_scanner->resetPeek(); + $this->_errors = array(); + } + + $this->token = null; + $this->lookahead = null; + + $this->_errorDistance = self::MIN_ERROR_DISTANCE; + } + + + /** + * Parses a query string. + */ + public function parse() + { + $this->lookahead = $this->_scanner->next(); + + // Building the Abstract Syntax Tree + // We have to double the call of QueryLanguage to allow it to work correctly... =\ + $AST = new Doctrine_Query_Production_QueryLanguage($this); + $AST = $AST->AST('QueryLanguage', Doctrine_Query_ProductionParamHolder::create()); + + // Check for end of string + if ($this->lookahead !== null) { + $this->syntaxError('end of string'); + } + + // Check for semantical errors + if (count($this->_errors) > 0) { + throw new Doctrine_Query_Parser_Exception(implode("\r\n", $this->_errors)); + } + + // Assign the SQL executor in parser result + $this->_parserResult->setSqlExecutor(Doctrine_Query_SqlExecutor_Abstract::create($AST)); + + return $this->_parserResult; + } + + + /** + * Retrieves the assocated Doctrine_Query_SqlBuilder to this object. + * + * @return Doctrine_Query_SqlBuilder + */ + public function getSqlBuilder() + { + return $this->_sqlBuilder; + } + + + /** + * Returns the scanner object associated with this object. + * + * @return Doctrine_Query_Scanner + */ + public function getScanner() + { + return $this->_scanner; + } + + + /** + * Returns the parser result associated with this object. + * + * @return Doctrine_Query_ParserResult + */ + public function getParserResult() + { + return $this->_parserResult; + } + + + /** + * Generates a new syntax error. + * + * @param string $expected Optional expected string. + * @param array $token Optional token. + */ + public function syntaxError($expected = '', $token = null) + { + if ($token === null) { + $token = $this->lookahead; + } + + // Formatting message + $message = 'line 0, col ' . (isset($token['position']) ? $token['position'] : '-1') . ': Error: '; + + if ($expected !== '') { + $message .= "Expected '$expected', got "; + } else { + $message .= 'Unexpected '; + } + + if ($this->lookahead === null) { + $message .= 'end of string.'; + } else { + $message .= "'{$this->lookahead['value']}'"; + } + + throw new Doctrine_Query_Parser_Exception($message); + } + + + /** + * Generates a new semantical error. + * + * @param string $message Optional message. + * @param array $token Optional token. + */ + public function semanticalError($message = '', $token = null) + { + $this->_semanticalErrorCount++; + + if ($token === null) { + $token = $this->token; + } + + $this->_logError('Warning: ' . $message, $token); + } + + + /** + * Logs new error entry. + * + * @param string $message Message to log. + * @param array $token Token that it was processing. + */ + protected function _logError($message = '', $token) + { + if ($this->_errorDistance >= self::MIN_ERROR_DISTANCE) { + $message = 'line 0, col ' . $token['position'] . ': ' . $message; + $this->_errors[] = $message; + } + + $this->_errorDistance = 0; + } + +} diff --git a/lib/Doctrine/Query/Offset.php b/lib/Doctrine/Query/Parser/Exception.php similarity index 75% rename from lib/Doctrine/Query/Offset.php rename to lib/Doctrine/Query/Parser/Exception.php index b09627d9f..9e5c449a3 100644 --- a/lib/Doctrine/Query/Offset.php +++ b/lib/Doctrine/Query/Parser/Exception.php @@ -1,39 +1,35 @@ -. - */ - -/** - * Doctrine_Query_Offset - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision: 1352 $ - * @author Konsta Vesterinen - */ -class Doctrine_Query_Offset extends Doctrine_Query_Part -{ - public function parse($offset) - { - return (int) $offset; - } -} \ No newline at end of file +. + */ + +/** + * Doctrine_Query_Parser_Exception + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Parser_Exception extends Doctrine_Exception +{ +} diff --git a/lib/Doctrine/Query/ParserResult.php b/lib/Doctrine/Query/ParserResult.php new file mode 100755 index 000000000..93231293e --- /dev/null +++ b/lib/Doctrine/Query/ParserResult.php @@ -0,0 +1,86 @@ +. + */ + +Doctrine::autoload('Doctrine_Query_AbstractResult'); + +/** + * Doctrine_Query_ParserResult + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 2.0 + * @version $Revision$ + */ +class Doctrine_Query_ParserResult extends Doctrine_Query_AbstractResult +{ + /** + * A simple array keys representing table aliases and values table alias + * seeds. The seeds are used for generating short table aliases. + * + * @var array $_tableAliasSeeds + */ + protected $_tableAliasSeeds = array(); + + + /** + * @nodoc + */ + public function setSqlExecutor(Doctrine_Query_SqlExecutor_Abstract $executor) + { + $this->_data = $executor; + } + + + /** + * @nodoc + */ + public function getSqlExecutor() + { + return $this->_data; + } + + + /** + * Generates a table alias from given table name and associates + * it with given component alias + * + * @param string $componentName Component name to be associated with generated table alias + * @return string Generated table alias + */ + public function generateTableAlias($componentName) + { + $baseAlias = strtolower(preg_replace('/[^A-Z]/', '\\1', $componentName)); + + $alias = $baseAlias; + + if ( ! isset($this->_tableAliasSeeds[$baseAlias])) { + $this->_tableAliasSeeds[$baseAlias] = 1; + } else { + $alias .= $this->_tableAliasSeeds[$baseAlias]++; + } + + return $alias; + } +} diff --git a/lib/Doctrine/Query/Printer.php b/lib/Doctrine/Query/Printer.php new file mode 100644 index 000000000..d68cc5fc6 --- /dev/null +++ b/lib/Doctrine/Query/Printer.php @@ -0,0 +1,95 @@ +. + */ + +/** + * A parse tree printer for Doctrine Query Language parser. + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Printer +{ + /** + * Current indentation level + * + * @var int + */ + protected $_indent = 0; + + /** + * Defines whether parse tree is printed (default, false) or not (true). + * + * @var bool + */ + protected $_silent; + + /** + * Constructs a new parse tree printer. + * + * @param bool $silent Parse tree will not be printed if true. + */ + public function __construct($silent = false) + { + $this->_silent = $silent; + } + + /** + * Prints an opening parenthesis followed by production name and increases + * indentation level by one. + * + * This method is called before executing a production. + * + * @param string $name production name + */ + public function startProduction($name) + { + $this->println('(' . $name); + $this->_indent++; + } + + /** + * Decreases indentation level by one and prints a closing parenthesis. + * + * This method is called after executing a production. + */ + public function endProduction() + { + $this->_indent--; + $this->println(')'); + } + + /** + * Prints text indented with spaces depending on current indentation level. + * + * @param string $str text + */ + public function println($str) + { + if ( ! $this->_silent) { + echo str_repeat(' ', $this->_indent), $str, "\n"; + } + } +} diff --git a/lib/Doctrine/Query/Production.php b/lib/Doctrine/Query/Production.php new file mode 100644 index 000000000..5f7f20590 --- /dev/null +++ b/lib/Doctrine/Query/Production.php @@ -0,0 +1,224 @@ +. + */ + +/** + * An abstract base class for the productions of the Doctrine Query Language + * context-free grammar. + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +abstract class Doctrine_Query_Production +{ + /** + * Parser object + * + * @var Doctrine_Query_Parser + */ + protected $_parser; + + + /** + * Creates a new production object. + * + * @param Doctrine_Query_Parser $parser a parser object + */ + public function __construct(Doctrine_Query_Parser $parser) + { + $this->_parser = $parser; + } + + + protected function _isNextToken($token) + { + $la = $this->_parser->lookahead; + return ($la['type'] === $token || $la['value'] === $token); + } + + + protected function _isFunction() + { + $la = $this->_parser->lookahead; + $next = $this->_parser->getScanner()->peek(); + return ($la['type'] === Doctrine_Query_Token::T_IDENTIFIER && $next['value'] === '('); + } + + + protected function _isSubselect() + { + $la = $this->_parser->lookahead; + $next = $this->_parser->getScanner()->peek(); + return ($la['value'] === '(' && $next['type'] === Doctrine_Query_Token::T_SELECT); + } + + + /** + * Executes the production AST using the specified parameters. + * + * @param string $AstName Production AST name + * @param array $paramHolder Production parameter holder + * @return Doctrine_Query_Production + */ + public function AST($AstName, $paramHolder) + { + $AST = $this->_getProduction($AstName); + + //echo "Processing class: " . get_class($AST) . "...\n"; + //echo "Params: " . var_export($paramHolder, true) . "\n"; + + // Syntax check + if ( ! $paramHolder->has('syntaxCheck') || $paramHolder->get('syntaxCheck') === true) { + //echo "Processing syntax checks of " . $AstName . "...\n"; + + $return = $AST->syntax($paramHolder); + + if ($return !== null) { + //echo "Returning AST class: " . (is_object($return) ? get_class($return) : $return) . "...\n"; + + return $return; + } + } + + // Semantical check + if ( ! $paramHolder->has('semanticalCheck') || $paramHolder->get('semanticalCheck') === true) { + //echo "Processing semantical checks of " . $AstName . "...\n"; + + $return = $AST->semantical($paramHolder); + + if ($return !== null) { + //echo "Returning AST class: " . (is_object($return) ? get_class($return) : $return) . "...\n"; + + return $return; + } + } + + //echo "Returning AST class: " . get_class($AST) . "...\n"; + + return $AST; + } + + + /** + * Returns a production object with the given name. + * + * @param string $name production name + * @return Doctrine_Query_Production + */ + protected function _getProduction($name) + { + $class = 'Doctrine_Query_Production_' . $name; + + return new $class($this->_parser); + } + + + /** + * Executes a production with specified name and parameters. + * + * @param string $name production name + * @param array $params an associative array containing parameter names and + * their values + * @return mixed + */ + public function __call($method, $args) + { + if (substr($method, 0, 3) === 'get') { + $var = '_' . substr($method, 3); + $var[1] = strtolower($var[1]); + + return $this->$var; + } + + return null; + } + + + /** + * Executes this production using the specified parameters. + * + * @param array $paramHolder Production parameter holder + * @return Doctrine_Query_Production + */ + public function execute($paramHolder) + { + //echo "Processing class: " . get_class($this) . " params: \n" . var_export($paramHolder, true) . "\n"; + + // Syntax check + if ( ! $paramHolder->has('syntaxCheck') || $paramHolder->get('syntaxCheck') === true) { + //echo "Processing syntax checks of " . get_class($this) . "...\n"; + + $return = $this->syntax($paramHolder); + + if ($return !== null) { + return $return; + } + } + + // Semantical check + if ( ! $paramHolder->has('semanticalCheck') || $paramHolder->get('semanticalCheck') === true) { + //echo "Processing semantical checks of " . get_class($this) . "...\n"; + + $return = $this->semantical($paramHolder); + + if ($return !== null) { + return $return; + } + } + + // Return AST instance + return $this; + } + + + /** + * Executes this sql builder using the specified parameters. + * + * @return string Sql piece + */ + public function buildSql() + { + $className = get_class($this); + $methodName = substr($className, strrpos($className, '_')); + + $this->_sqlBuilder->$methodName($this); + } + + + /** + * @nodoc + */ + abstract public function syntax($paramHolder); + + + /** + * @nodoc + */ + public function semantical($paramHolder) + { + } +} diff --git a/lib/Doctrine/Query/Production/AggregateExpression.php b/lib/Doctrine/Query/Production/AggregateExpression.php new file mode 100644 index 000000000..e9763a5f7 --- /dev/null +++ b/lib/Doctrine/Query/Production/AggregateExpression.php @@ -0,0 +1,84 @@ +. + */ + +/** + * AggregateExpression = ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] Expression ")" + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_AggregateExpression extends Doctrine_Query_Production +{ + protected $_functionName; + + protected $_isDistinct; + + protected $_expression; + + + public function syntax($paramHolder) + { + // AggregateExpression = ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] Expression ")" + $this->_isDistinct = false; + $token = $this->_parser->lookahead; + + switch ($token['type']) { + case Doctrine_Query_Token::T_AVG: + case Doctrine_Query_Token::T_MAX: + case Doctrine_Query_Token::T_MIN: + case Doctrine_Query_Token::T_SUM: + case Doctrine_Query_Token::T_COUNT: + $this->_parser->match($token['type']); + $this->_functionName = strtoupper($token['value']); + break; + + default: + $this->_parser->logError('AVG, MAX, MIN, SUM or COUNT'); + break; + } + + $this->_parser->match('('); + + if ($this->_isNextToken(Doctrine_Query_Token::T_DISTINCT)) { + $this->_parser->match(Doctrine_Query_Token::T_DISTINCT); + $this->_isDistinct = true; + } + + $this->_expression = $this->AST('Expression', $paramHolder); + + $this->_parser->match(')'); + } + + + public function buildSql() + { + return $this->_functionName + . '(' . (($this->_isDistinct) ? 'DISTINCT ' : '') + . $this->_expression->buildSql() + . ')'; + } +} diff --git a/lib/Doctrine/Query/Production/Atom.php b/lib/Doctrine/Query/Production/Atom.php new file mode 100644 index 000000000..142f3ae41 --- /dev/null +++ b/lib/Doctrine/Query/Production/Atom.php @@ -0,0 +1,95 @@ +. + */ + +/** + * Atom = string | integer | float | input_parameter + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_Atom extends Doctrine_Query_Production +{ + protected $_type; + + protected $_value; + + + public function syntax($paramHolder) + { + // Atom = string | integer | float | input_parameter + switch ($this->_parser->lookahead['type']) { + case Doctrine_Query_Token::T_STRING: + $this->_parser->match(Doctrine_Query_Token::T_STRING); + $this->_type = 'string'; + break; + + case Doctrine_Query_Token::T_INTEGER: + $this->_parser->match(Doctrine_Query_Token::T_INTEGER); + $this->_type = 'integer'; + break; + + case Doctrine_Query_Token::T_FLOAT: + $this->_parser->match(Doctrine_Query_Token::T_FLOAT); + $this->_type = 'float'; + break; + + case Doctrine_Query_Token::T_INPUT_PARAMETER: + $this->_parser->match(Doctrine_Query_Token::T_INPUT_PARAMETER); + $this->_type = 'param'; + break; + + default: + $this->_parser->syntaxError('string, number or parameter (? or :)'); + break; + } + + $this->_value = $this->_parser->token['value']; + } + + + public function buildSql() + { + $conn = $this->_parser->getSqlBuilder()->getConnection(); + + switch ($this->_type) { + case 'param': + return $this->_value; + break; + + case 'integer': + case 'float': + return $conn->quote($this->_value, $this->_type); + break; + + default: + return $conn->string_quoting['start'] + . $conn->quote($this->_value, $this->_type) + . $conn->string_quoting['end']; + break; + } + } +} diff --git a/lib/Doctrine/Query/Production/BetweenExpression.php b/lib/Doctrine/Query/Production/BetweenExpression.php new file mode 100644 index 000000000..9952552c7 --- /dev/null +++ b/lib/Doctrine/Query/Production/BetweenExpression.php @@ -0,0 +1,68 @@ +. + */ + +/** + * BetweenExpression = ["NOT"] "BETWEEN" Expression "AND" Expression + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_BetweenExpression extends Doctrine_Query_Production +{ + protected $_not; + + protected $_fromExpression; + + protected $_toExpression; + + + public function syntax($paramHolder) + { + // BetweenExpression = ["NOT"] "BETWEEN" Expression "AND" Expression + $this->_not = false; + + if ($this->_isNextToken(Doctrine_Query_Token::T_NOT)) { + $this->_parser->match(Doctrine_Query_Token::T_NOT); + $this->_not = true; + } + + $this->_parser->match(Doctrine_Query_Token::T_BETWEEN); + + $this->_fromExpression = $this->AST('Expression', $paramHolder); + + $this->_parser->match(Doctrine_Query_Token::T_AND); + + $this->_toExpression = $this->AST('Expression', $paramHolder); + } + + + public function buildSql() + { + return (($this->_not) ? 'NOT ' : '') . 'BETWEEN ' + . $this->_fromExpression->buildSql() . ' AND ' . $this->_toExpression->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/ComparisonExpression.php b/lib/Doctrine/Query/Production/ComparisonExpression.php new file mode 100644 index 000000000..8aa27c3a0 --- /dev/null +++ b/lib/Doctrine/Query/Production/ComparisonExpression.php @@ -0,0 +1,75 @@ +. + */ + +/** + * ComparisonExpression = ComparisonOperator ( QuantifiedExpression | Expression | "(" Subselect ")" ) + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_ComparisonExpression extends Doctrine_Query_Production +{ + protected $_operator; + + protected $_expression; + + protected $_isSubselect; + + + public function syntax($paramHolder) + { + // ComparisonExpression = ComparisonOperator ( QuantifiedExpression | Expression | "(" Subselect ")" ) + $this->_operator = $this->AST('ComparisonOperator', $paramHolder); + + if (($this->_isSubselect = $this->_isSubselect()) === true) { + $this->_parser->match('('); + $this->_expression = $this->AST('Subselect', $paramHolder); + $this->_parser->match(')'); + + $this->_isSubselect = true; + } else { + switch ($this->_parser->lookahead['type']) { + case Doctrine_Query_Token::T_ALL: + case Doctrine_Query_Token::T_ANY: + case Doctrine_Query_Token::T_SOME: + $this->_expression = $this->AST('QuantifiedExpression', $paramHolder); + break; + + default: + $this->_expression = $this->AST('Expression', $paramHolder); + break; + } + } + } + + + public function buildSql() + { + return $this->_operator . ' ' . (($this->_isSubselect) ? + '(' . $this->_expression->buildSql() . ')' : $this->_expression->buildSql() + ); + } +} diff --git a/lib/Doctrine/Query/Production/ComparisonOperator.php b/lib/Doctrine/Query/Production/ComparisonOperator.php new file mode 100644 index 000000000..d096e0267 --- /dev/null +++ b/lib/Doctrine/Query/Production/ComparisonOperator.php @@ -0,0 +1,81 @@ +. + */ + +/** + * ComparisonOperator = "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_ComparisonOperator extends Doctrine_Query_Production +{ + public function syntax($paramHolder) + { + switch ($this->_parser->lookahead['value']) { + case '=': + $this->_parser->match('='); + return '='; + break; + + case '<': + $this->_parser->match('<'); + $operator = '<'; + + if ($this->_isNextToken('=')) { + $this->_parser->match('='); + $operator .= '='; + } elseif ($this->_isNextToken('>')) { + $this->_parser->match('>'); + $operator .= '>'; + } + + return $operator; + break; + + case '>': + $this->_parser->match('>'); + $operator = '>'; + + if ($this->_isNextToken('=')) { + $this->_parser->match('='); + $operator .= '='; + } + + return $operator; + break; + + case '!': + $this->_parser->match('!'); + $this->_parser->match('='); + return '<>'; + break; + + default: + $this->_parser->syntaxError('=, <, <=, <>, >, >=, !='); + break; + } + } +} diff --git a/lib/Doctrine/Query/Production/ConditionalExpression.php b/lib/Doctrine/Query/Production/ConditionalExpression.php new file mode 100644 index 000000000..b15333bc3 --- /dev/null +++ b/lib/Doctrine/Query/Production/ConditionalExpression.php @@ -0,0 +1,72 @@ +. + */ + +/** + * ConditionalExpression = ConditionalTerm {"OR" ConditionalTerm} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_ConditionalExpression extends Doctrine_Query_Production +{ + protected $_conditionalTerms = array(); + + + public function syntax($paramHolder) + { + // ConditionalExpression = ConditionalTerm {"OR" ConditionalTerm} + $this->_conditionalTerms[] = $this->AST('ConditionalTerm', $paramHolder); + + while ($this->_isNextToken(Doctrine_Query_Token::T_OR)) { + $this->_parser->match(Doctrine_Query_Token::T_OR); + $this->_conditionalTerms[] = $this->AST('ConditionalTerm', $paramHolder); + } + + // Optimize depth instances in AST + if (count($this->_conditionalTerms) == 1) { + return $this->_conditionalTerms[0]; + } + } + + + public function buildSql() + { + return implode(' OR ', $this->_mapConditionalTerms()); + } + + + protected function _mapConditionalTerms() + { + return array_map(array(&$this, '_mapConditionalTerm'), $this->_conditionalTerms); + } + + + protected function _mapConditionalTerm($value) + { + return $value->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/ConditionalFactor.php b/lib/Doctrine/Query/Production/ConditionalFactor.php new file mode 100644 index 000000000..efce96b02 --- /dev/null +++ b/lib/Doctrine/Query/Production/ConditionalFactor.php @@ -0,0 +1,63 @@ +. + */ + +/** + * ConditionalFactor = ["NOT"] ConditionalPrimary + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_ConditionalFactor extends Doctrine_Query_Production +{ + protected $_conditionalPrimary; + + + public function syntax($paramHolder) + { + // ConditionalFactor = ["NOT"] ConditionalPrimary + $notFactor = false; + + if ($this->_isNextToken(Doctrine_Query_Token::T_NOT)) { + $this->_parser->match(Doctrine_Query_Token::T_NOT); + $notFactor = true; + } + + $this->_conditionalPrimary = $this->AST('ConditionalPrimary', $paramHolder); + + // Optimize depth instances in AST + if ( ! $notFactor) { + return $this->_conditionalPrimary; + } + } + + + public function buildSql() + { + // Do not need to check $notFactor. It'll be always present if we have this instance. + return 'NOT ' . $this->_conditionalPrimary->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/ConditionalPrimary.php b/lib/Doctrine/Query/Production/ConditionalPrimary.php new file mode 100644 index 000000000..028e11c5d --- /dev/null +++ b/lib/Doctrine/Query/Production/ConditionalPrimary.php @@ -0,0 +1,101 @@ +. + */ + +/** + * ConditionalPrimary = SimpleConditionalExpression | "(" ConditionalExpression ")" + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_ConditionalPrimary extends Doctrine_Query_Production +{ + protected $_conditionalExpression; + + + public function syntax($paramHolder) + { + // ConditionalPrimary = SimpleConditionalExpression | "(" ConditionalExpression ")" + if ( ! $this->_isConditionalExpression()) { + return $this->AST('SimpleConditionalExpression', $paramHolder); + } + + $this->_parser->match('('); + $this->_conditionalExpression = $this->AST('ConditionalExpression', $paramHolder); + $this->_parser->match(')'); + } + + + public function buildSql() + { + return '(' . $this->_conditionalExpression->buildSql() . ')'; + } + + + protected function _isConditionalExpression() + { + $token = $this->_parser->lookahead; + $parenthesis = 0; + + if ($token['value'] === '(') { + $parenthesis++; + } + + while ($parenthesis > 0) { + $token = $this->_parser->getScanner()->peek(); + + if ($token['value'] === '(') { + $parenthesis++; + } elseif ($token['value'] === ')') { + $parenthesis--; + } else { + switch ($token['type']) { + case Doctrine_Query_Token::T_NOT: + case Doctrine_Query_Token::T_AND: + case Doctrine_Query_Token::T_OR: + case Doctrine_Query_Token::T_BETWEEN: + case Doctrine_Query_Token::T_LIKE: + case Doctrine_Query_Token::T_IN: + case Doctrine_Query_Token::T_IS: + case Doctrine_Query_Token::T_EXISTS: + return true; + + case Doctrine_Query_Token::T_NONE: + switch ($token['value']) { + case '=': + case '<': + case '>': + case '!': + return true; + } + break; + } + } + } + + return false; + } +} diff --git a/lib/Doctrine/Query/Production/ConditionalTerm.php b/lib/Doctrine/Query/Production/ConditionalTerm.php new file mode 100644 index 000000000..5c0a2d4ab --- /dev/null +++ b/lib/Doctrine/Query/Production/ConditionalTerm.php @@ -0,0 +1,72 @@ +. + */ + +/** + * ConditionalTerm = ConditionalFactor {"AND" ConditionalFactor} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_ConditionalTerm extends Doctrine_Query_Production +{ + protected $_conditionalFactors = array(); + + + public function syntax($paramHolder) + { + // ConditionalTerm = ConditionalFactor {"AND" ConditionalFactor} + $this->_conditionalFactors[] = $this->AST('ConditionalFactor', $paramHolder); + + while ($this->_isNextToken(Doctrine_Query_Token::T_AND)) { + $this->_parser->match(Doctrine_Query_Token::T_AND); + $this->_conditionalFactors[] = $this->AST('ConditionalFactor', $paramHolder); + } + + // Optimize depth instances in AST + if (count($this->_conditionalFactors) == 1) { + return $this->_conditionalFactors[0]; + } + } + + + public function buildSql() + { + return implode(' AND ', $this->_mapConditionalFactors()); + } + + + protected function _mapConditionalFactors() + { + return array_map(array(&$this, '_mapConditionalFactor'), $this->_conditionalFactors); + } + + + protected function _mapConditionalFactor($value) + { + return $value->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Part.php b/lib/Doctrine/Query/Production/DeleteClause.php similarity index 57% rename from lib/Doctrine/Query/Part.php rename to lib/Doctrine/Query/Production/DeleteClause.php index a6cd4a21e..4d38f4a68 100644 --- a/lib/Doctrine/Query/Part.php +++ b/lib/Doctrine/Query/Production/DeleteClause.php @@ -1,61 +1,56 @@ -. - */ - -/** - * Doctrine_Query_Part - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - * @author Konsta Vesterinen - */ -abstract class Doctrine_Query_Part -{ - /** - * @var Doctrine_Query $query the query object associated with this parser - */ - protected $query; - - protected $_tokenizer; - - /** - * @param Doctrine_Query $query the query object associated with this parser - */ - public function __construct($query, Doctrine_Query_Tokenizer $tokenizer = null) - { - $this->query = $query; - if ( ! $tokenizer) { - $tokenizer = new Doctrine_Query_Tokenizer(); - } - $this->_tokenizer = $tokenizer; - } - - /** - * @return Doctrine_Query $query the query object associated with this parser - */ - public function getQuery() - { - return $this->query; - } -} +. + */ + +/** + * DeleteClause = "DELETE" ["FROM"] VariableDeclaration + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_DeleteClause extends Doctrine_Query_Production +{ + protected $_variableDeclaration; + + + public function syntax($paramHolder) + { + // DeleteClause = "DELETE" ["FROM"] VariableDeclaration + $this->_parser->match(Doctrine_Query_Token::T_DELETE); + + if ($this->_isNextToken(Doctrine_Query_Token::T_FROM)) { + $this->_parser->match(Doctrine_Query_Token::T_FROM); + } + + $this->_variableDeclaration = $this->AST('VariableDeclaration', $paramHolder); + } + + + public function buildSql() + { + return 'DELETE FROM ' . $this->_variableDeclaration->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/DeleteStatement.php b/lib/Doctrine/Query/Production/DeleteStatement.php new file mode 100644 index 000000000..8236f30ac --- /dev/null +++ b/lib/Doctrine/Query/Production/DeleteStatement.php @@ -0,0 +1,59 @@ +. + */ + +/** + * DeleteStatement = DeleteClause [WhereClause] + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_DeleteStatement extends Doctrine_Query_Production +{ + protected $_deleteClause; + + protected $_whereClause; + + + public function syntax($paramHolder) + { + // DeleteStatement = DeleteClause [WhereClause] + $this->_deleteClause = $this->AST('DeleteClause', $paramHolder); + + if ($this->_isNextToken(Doctrine_Query_Token::T_WHERE)) { + $this->_whereClause = $this->AST('WhereClause', $paramHolder); + } + } + + + public function buildSql() + { + // The 1=1 is needed to workaround the affected_rows in MySQL. + // Simple "DELETE FROM table_name" gives 0 affected rows. + return $this->_deleteClause->buildSql() . (($this->_whereClause !== null) + ? $this->_whereClause->buildSql() : ' WHERE 1 = 1'); + } +} diff --git a/lib/Doctrine/Query/Production/ExistsExpression.php b/lib/Doctrine/Query/Production/ExistsExpression.php new file mode 100644 index 000000000..155074f74 --- /dev/null +++ b/lib/Doctrine/Query/Production/ExistsExpression.php @@ -0,0 +1,54 @@ +. + */ + +/** + * ExistsExpression = "EXISTS" "(" Subselect ")" + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_ExistsExpression extends Doctrine_Query_Production +{ + protected $_subselect; + + + public function syntax($paramHolder) + { + // ExistsExpression = "EXISTS" "(" Subselect ")" + $this->_parser->match(Doctrine_Query_Token::T_EXISTS); + + $this->_parser->match('('); + $this->_subselect = $this->AST('Subselect', $paramHolder); + $this->_parser->match(')'); + } + + + public function buildSql() + { + return 'EXISTS (' . $this->_subselect->buildSql() . ')'; + } +} diff --git a/lib/Doctrine/Query/Production/Expression.php b/lib/Doctrine/Query/Production/Expression.php new file mode 100644 index 000000000..18e82efb9 --- /dev/null +++ b/lib/Doctrine/Query/Production/Expression.php @@ -0,0 +1,79 @@ +. + */ + +/** + * Expression = Term {("+" | "-") Term} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_Expression extends Doctrine_Query_Production +{ + protected $_terms = array(); + + + public function syntax($paramHolder) + { + // Expression = Term {("+" | "-") Term} + $this->_terms[] = $this->AST('Term', $paramHolder); + + while ($this->_isNextToken('+') || $this->_isNextToken('-')) { + if ($this->_isNextToken('+')) { + $this->_parser->match('+'); + $this->_terms[] = '+'; + } else{ + $this->_parser->match('-'); + $this->_terms[] = '-'; + } + + $this->_terms[] = $this->AST('Term', $paramHolder); + } + + // Optimize depth instances in AST + if (count($this->_terms) == 1) { + return $this->_terms[0]; + } + } + + + public function buildSql() + { + return implode(' ', $this->_mapTerms()); + } + + + protected function _mapTerms() + { + return array_map(array(&$this, '_mapTerm'), $this->_terms); + } + + + protected function _mapTerm($value) + { + return (is_string($value) ? $value : $value->buildSql()); + } +} diff --git a/lib/Doctrine/Query/Production/Factor.php b/lib/Doctrine/Query/Production/Factor.php new file mode 100644 index 000000000..49215b8a3 --- /dev/null +++ b/lib/Doctrine/Query/Production/Factor.php @@ -0,0 +1,65 @@ +. + */ + +/** + * Factor = [("+" | "-")] Primary + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_Factor extends Doctrine_Query_Production +{ + protected $_type; + + protected $_primary; + + + public function syntax($paramHolder) + { + // Factor = [("+" | "-")] Primary + if ($this->_isNextToken('+')) { + $this->_parser->match('+'); + $this->_type = '+'; + } elseif ($this->_isNextToken('-')) { + $this->_parser->match('-'); + $this->_type = '-'; + } + + $this->_primary = $this->AST('Primary', $paramHolder); + + // Optimize depth instances in AST + if ($this->_type === null) { + return $this->_primary; + } + } + + + public function buildSql() + { + return $this->_type . ' ' . $this->_primary->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/FromClause.php b/lib/Doctrine/Query/Production/FromClause.php new file mode 100644 index 000000000..81399b447 --- /dev/null +++ b/lib/Doctrine/Query/Production/FromClause.php @@ -0,0 +1,74 @@ +. + */ + +/** + * FromClause = "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_FromClause extends Doctrine_Query_Production +{ + protected $_identificationVariableDeclaration = array(); + + + public function syntax($paramHolder) + { + // FromClause = "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration} + $this->_parser->match(Doctrine_Query_Token::T_FROM); + + $this->_identificationVariableDeclaration[] = $this->AST('IdentificationVariableDeclaration', $paramHolder); + + while ($this->_isNextToken(',')) { + $this->_parser->match(','); + $this->_identificationVariableDeclaration[] = $this->AST('IdentificationVariableDeclaration', $paramHolder); + } + } + + + public function buildSql() + { + echo "FromClause:\n"; + for ($i = 0; $i < count($this->_identificationVariableDeclaration);$i++) { + echo (($this->_identificationVariableDeclaration[$i] instanceof IdentificationVariableDeclaration) ? get_class($this->_identificationVariableDeclaration[$i]) : get_class($this->_identificationVariableDeclaration[$i])) . "\n"; + } + + return 'FROM ' . implode(', ', $this->_mapIdentificationVariableDeclarations()); + } + + + protected function _mapIdentificationVariableDeclarations() + { + return array_map(array(&$this, '_mapIdentificationVariableDeclaration'), $this->_identificationVariableDeclaration); + } + + + protected function _mapIdentificationVariableDeclaration($value) + { + return $value->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/Function.php b/lib/Doctrine/Query/Production/Function.php new file mode 100644 index 000000000..44363adf7 --- /dev/null +++ b/lib/Doctrine/Query/Production/Function.php @@ -0,0 +1,79 @@ +. + */ + +/** + * Function = identifier "(" [Expression {"," Expression}] ")" + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_Function extends Doctrine_Query_Production +{ + protected $_functionName; + + protected $_arguments = array(); + + + public function syntax($paramHolder) + { + // Function = identifier "(" [Expression {"," Expression}] ")" + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + $this->_functionName = $this->_parser->token['value']; + + $this->_parser->match('('); + + if ( ! $this->_isNextToken(')')) { + $this->_arguments[] = $this->AST('Expression', $paramHolder); + + while ($this->_isNextToken(',')) { + $this->_parser->match(','); + + $this->_arguments[] = $this->AST('Expression', $paramHolder); + } + } + + $this->_parser->match(')'); + } + + + public function buildSql() + { + return $this->_functionName . '(' . implode(', ', $this->_mapArguments()) . ')'; + } + + + protected function _mapArguments() + { + return array_map(array(&$this, '_mapArgument'), $this->_arguments); + } + + + protected function _mapArgument($value) + { + return $value->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/GroupByClause.php b/lib/Doctrine/Query/Production/GroupByClause.php new file mode 100644 index 000000000..d648de144 --- /dev/null +++ b/lib/Doctrine/Query/Production/GroupByClause.php @@ -0,0 +1,69 @@ +. + */ + +/** + * GroupByClause = "GROUP" "BY" GroupByItem {"," GroupByItem} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_GroupByClause extends Doctrine_Query_Production +{ + protected $_groupByItems = array(); + + + public function syntax($paramHolder) + { + $this->_parser->match(Doctrine_Query_Token::T_GROUP); + $this->_parser->match(Doctrine_Query_Token::T_BY); + + $this->_groupByItems[] = $this->AST('GroupByItem', $paramHolder); + + while ($this->_isNextToken(',')) { + $this->_parser->match(','); + $this->_groupByItems[] = $this->AST('GroupByItem', $paramHolder); + } + } + + + public function buildSql() + { + return 'GROUP BY ' . implode(', ', $this->_mapGroupByItems()) . ')'; + } + + + protected function _mapGroupByItems() + { + return array_map(array(&$this, '_mapGroupByItem'), $this->_groupByItems); + } + + + protected function _mapGroupByItem($value) + { + return $value->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Select.php b/lib/Doctrine/Query/Production/GroupByItem.php similarity index 73% rename from lib/Doctrine/Query/Select.php rename to lib/Doctrine/Query/Production/GroupByItem.php index 3889fc903..3c74bb3fe 100644 --- a/lib/Doctrine/Query/Select.php +++ b/lib/Doctrine/Query/Production/GroupByItem.php @@ -1,39 +1,40 @@ -. - */ -Doctrine::autoload("Doctrine_Query_Part"); -/** - * Doctrine_Query_Select - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision: 1080 $ - * @author Konsta Vesterinen - */ -class Doctrine_Query_Select extends Doctrine_Query_Part -{ - public function parse($dql) - { - $this->query->parseSelect($dql); - } -} \ No newline at end of file +. + */ + +/** + * OrderByItem = PathExpression + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_GroupByItem extends Doctrine_Query_Production +{ + public function syntax($paramHolder) + { + return $this->AST('PathExpression', $paramHolder); + } +} diff --git a/lib/Doctrine/Query/Production/HavingClause.php b/lib/Doctrine/Query/Production/HavingClause.php new file mode 100644 index 000000000..3a095d8ad --- /dev/null +++ b/lib/Doctrine/Query/Production/HavingClause.php @@ -0,0 +1,52 @@ +. + */ + +/** + * HavingClause = "HAVING" ConditionalExpression + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_HavingClause extends Doctrine_Query_Production +{ + protected $_conditionalExpression; + + + public function syntax($paramHolder) + { + // HavingClause = "HAVING" ConditionalExpression + $this->_parser->match(Doctrine_Query_Token::T_HAVING); + + $this->_conditionalExpression = $this->AST('ConditionalExpression', $paramHolder); + } + + + public function buildSql() + { + return 'HAVING ' . $this->_conditionalExpression->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/IdentificationVariable.php b/lib/Doctrine/Query/Production/IdentificationVariable.php new file mode 100644 index 000000000..db6361c25 --- /dev/null +++ b/lib/Doctrine/Query/Production/IdentificationVariable.php @@ -0,0 +1,64 @@ +. + */ + +/** + * IdentificationVariable = identifier + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_IdentificationVariable extends Doctrine_Query_Production +{ + protected $_componentAlias; + + + public function syntax($paramHolder) + { + // IdentificationVariable = identifier + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + $this->_componentAlias = $this->_parser->token['value']; + } + + + public function semantical($paramHolder) + { + $parserResult = $this->_parser->getParserResult(); + + if ($parserResult->hasQueryComponent($this->_componentAlias)) { + // We should throw semantical error if there's already a component for this alias + $queryComponent = $parserResult->getQueryComponent($this->_componentAlias); + $componentName = $queryComponent['metadata']->getClassName(); + + $message = "Cannot re-declare component alias '{$this->_componentAlias}'" + . "for '".$paramHolder->get('componentName')."'. It was already declared for " + . "component '{$componentName}'."; + + $this->_parser->semanticalError($message); + } + + return $this->_componentAlias; + } +} diff --git a/lib/Doctrine/Query/Production/IdentificationVariableDeclaration.php b/lib/Doctrine/Query/Production/IdentificationVariableDeclaration.php new file mode 100644 index 000000000..c8bb8735e --- /dev/null +++ b/lib/Doctrine/Query/Production/IdentificationVariableDeclaration.php @@ -0,0 +1,95 @@ +. + */ + +/** + * IdentificationVariableDeclaration = RangeVariableDeclaration [IndexBy] {Join [IndexBy]} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_IdentificationVariableDeclaration extends Doctrine_Query_Production +{ + protected $_rangeVariableDeclaration; + + protected $_indexBy; + + protected $_relations = array(); + + + public function syntax($paramHolder) + { + $this->_rangeVariableDeclaration = $this->AST('RangeVariableDeclaration', $paramHolder); + + if ($this->_isNextToken(Doctrine_Query_Token::T_INDEX)) { + $paramHolder->set('componentAlias', $this->_rangeVariableDeclaration); + $this->_indexBy = $this->AST('IndexBy', $paramHolder); + $paramHolder->remove('componentAlias'); + } + + while ( + $this->_isNextToken(Doctrine_Query_Token::T_LEFT) || + $this->_isNextToken(Doctrine_Query_Token::T_INNER) || + $this->_isNextToken(Doctrine_Query_Token::T_JOIN) + ) { + $i = count($this->_relations); + + $this->_relations[$i]['join'] = $this->AST('Join', $paramHolder); + + if ($this->_isNextToken(Doctrine_Query_Token::T_INDEX)) { + $paramHolder->set('componentAlias', $this->_relations[$i]['join']->getRangeVariableDeclaration()); + $this->_relations[$i]['indexBy'] = $this->AST('IndexBy', $paramHolder); + $paramHolder->remove('componentAlias'); + } + } + } + + + public function buildSql() + { + // We need to bring the queryComponent and get things from there. + $parserResult = $this->_parser->getParserResult(); + $queryComponent = $parserResult->getQueryComponent($this->_rangeVariableDeclaration); + + // Retrieving connection + $conn = $this->_parser->getSqlBuilder()->getConnection(); + $manager = Doctrine_Manager::getInstance(); + + if ($manager->hasConnectionForComponent($queryComponent['metadata']->getClassName())) { + $conn = $manager->getConnectionForComponent($queryComponent['metadata']->getClassName()); + } + + $str = $conn->quoteIdentifier($queryComponent['metadata']->getTableName()) . ' ' + . $conn->quoteIdentifier($parserResult->getTableAliasFromComponentAlias($this->_rangeVariableDeclaration)); + + for ($i = 0, $l = count($this->_relations); $i < $l; $i++) { + $str .= $this->_relations[$i]['join']->buildSql() . ' ' + . ((isset($this->_relations[$i]['indexby'])) ? $this->_relations[$i]['indexby']->buildSql() . ' ' : ''); + } + + return $str; + } +} diff --git a/lib/Doctrine/Query/Production/InExpression.php b/lib/Doctrine/Query/Production/InExpression.php new file mode 100644 index 000000000..788c8f8b4 --- /dev/null +++ b/lib/Doctrine/Query/Production/InExpression.php @@ -0,0 +1,90 @@ +. + */ + +/** + * InExpression = ["NOT"] "IN" "(" (Atom {"," Atom} | Subselect) ")" + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_InExpression extends Doctrine_Query_Production +{ + protected $_not; + + protected $_subselect; + + protected $_atoms = array(); + + + public function syntax($paramHolder) + { + // InExpression = ["NOT"] "IN" "(" (Atom {"," Atom} | Subselect) ")" + $this->_not = false; + + if ($this->_isNextToken(Doctrine_Query_Token::T_NOT)) { + $this->_parser->match(Doctrine_Query_Token::T_NOT); + $this->_not = true; + } + + $this->_parser->match(Doctrine_Query_Token::T_IN); + + $this->_parser->match('('); + + if ($this->_isNextToken(Doctrine_Query_Token::T_SELECT)) { + $this->_subselect = $this->AST('Subselect', $paramHolder); + } else { + $this->_atoms[] = $this->AST('Atom', $paramHolder); + + while ($this->_isNextToken(',')) { + $this->_parser->match(','); + $this->_atoms[] = $this->AST('Atom', $paramHolder); + } + } + + $this->_parser->match(')'); + } + + + public function buildSql() + { + return (($this->_not) ? 'NOT ' : '') . 'IN (' + . (($this->_subselect !== null) ? $this->_subselect->buildSql() : implode(', ', $this->_mapAtoms())) + . ')'; + } + + + protected function _mapAtoms() + { + return array_map(array(&$this, '_mapAtom'), $this->_atoms); + } + + + protected function _mapAtom($value) + { + return $value->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/IndexBy.php b/lib/Doctrine/Query/Production/IndexBy.php new file mode 100644 index 000000000..b3eaf2ad3 --- /dev/null +++ b/lib/Doctrine/Query/Production/IndexBy.php @@ -0,0 +1,108 @@ +. + */ + +/** + * IndexBy = "INDEX" "BY" identifier + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_IndexBy extends Doctrine_Query_Production +{ + protected $_componentAlias; + + protected $_fieldName; + + + public function syntax($paramHolder) + { + $this->_componentAlias = $paramHolder->get('componentAlias'); + + $this->_parser->match(Doctrine_Query_Token::T_INDEX); + $this->_parser->match(Doctrine_Query_Token::T_BY); + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + + $this->_fieldName = $this->_parser->token['value']; + } + + + public function semantical($paramHolder) + { + $parserResult = $this->_parser->getParserResult(); + + //echo "Component alias: " . $this->_componentAlias . "\n"; + //echo "Has query component: " . ($parserResult->hasQueryComponent($this->_componentAlias) ? "TRUE" : "FALSE") . "\n"; + //$qc = $parserResult->getQueryComponents(); + //$qc = array_keys($qc); + //echo "Query Components: " . var_export($qc, true) . "\n"; + + try { + $queryComponent = $parserResult->getQueryComponent($this->_componentAlias); + $classMetadata = $queryComponent['metadata']; + } catch (Doctrine_Exception $e) { + $this->_parser->semanticalError($e->getMessage()); + + return; + } + + if ($classMetadata instanceof Doctrine_ClassMetadata && ! $classMetadata->hasField($this->_fieldName)) { + $this->_parser->semanticalError( + "Cannot use key mapping. Field '" . $this->_fieldName . "' " . + "does not exist in component '" . $classMetadata->getClassName() . "'.", + $this->_parser->token + ); + } + + // The INDEXBY field must be either the (primary && not part of composite pk) || (unique && notnull) + $columnMapping = $classMetadata->getColumnMapping($this->_fieldName); + + if ( ! $classMetadata->isIdentifier($field) && ! $classMetadata->isUniqueField($field) && ! $classMetadata->isNotNull($field)) { + $this->_parser->semanticalError( + "Field '" . $this->_fieldName . "' of component '" . $classMetadata->getClassName() . + "' must be unique and notnull to be used as index.", + $this->_parser->token + ); + } + + if ($classMetadata->isIdentifier($field) && $classMetadata->isIdentifierComposite()) { + $this->_parser->semanticalError( + "Field '" . $this->_fieldName . "' of component '" . $classMetadata->getClassName() . + "' must be primary and not part of a composite primary key to be used as index.", + $this->_parser->token + ); + } + + + $queryComponent['map'] = $this->_fieldName; + $parserResult->setQueryComponent($this->_componentAlias, $queryComponent); + } + + + public function buildSql() + { + return ''; + } +} diff --git a/lib/Doctrine/Query/Production/Join.php b/lib/Doctrine/Query/Production/Join.php new file mode 100644 index 000000000..ae7c6a37e --- /dev/null +++ b/lib/Doctrine/Query/Production/Join.php @@ -0,0 +1,79 @@ +. + */ + +/** + * Join = ["LEFT" | "INNER"] "JOIN" RangeVariableDeclaration [("ON" | "WITH") ConditionalExpression] + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_Join extends Doctrine_Query_Production +{ + protected $_joinType; + + protected $_rangeVariableDeclaration; + + protected $_whereType; + + protected $_conditionalExpression; + + + public function syntax($paramHolder) + { + $this->_joinType = 'INNER'; + $this->_whereType = 'WITH'; + + if ($this->_isNextToken(Doctrine_Query_Token::T_LEFT)) { + $this->_parser->match(Doctrine_Query_Token::T_LEFT); + + $this->_joinType = 'LEFT'; + } elseif ($this->_isNextToken(Doctrine_Query_Token::T_INNER)) { + $this->_parser->match(Doctrine_Query_Token::T_INNER); + } + + $this->_parser->match(Doctrine_Query_Token::T_JOIN); + + $this->_rangeVariableDeclaration = $this->AST('RangeVariableDeclaration', $paramHolder); + + if ($this->_isNextToken(Doctrine_Query_Token::T_ON)) { + $this->_parser->match(Doctrine_Query_Token::T_ON); + + $this->_whereType = 'ON'; + + $this->_conditionalExpression = $this->AST('ConditionalExpression', $paramHolder); + } elseif ($this->_isNextToken(Doctrine_Query_Token::T_WITH)) { + $this->_parser->match(Doctrine_Query_Token::T_WITH); + + $this->_conditionalExpression = $this->AST('ConditionalExpression', $paramHolder); + } + } + + + public function buildSql() + { + return ''; + } +} diff --git a/lib/Doctrine/Query/Production/LikeExpression.php b/lib/Doctrine/Query/Production/LikeExpression.php new file mode 100644 index 000000000..e31cb3dd8 --- /dev/null +++ b/lib/Doctrine/Query/Production/LikeExpression.php @@ -0,0 +1,72 @@ +. + */ + +/** + * LikeExpression = ["NOT"] "LIKE" Expression ["ESCAPE" string] + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_LikeExpression extends Doctrine_Query_Production +{ + protected $_not; + + protected $_expression; + + protected $_escapeString; + + + public function syntax($paramHolder) + { + // LikeExpression = ["NOT"] "LIKE" Expression ["ESCAPE" string] + $this->_escapeString = null; + $this->_not = false; + + if ($this->_isNextToken(Doctrine_Query_Token::T_NOT)) { + $this->_parser->match(Doctrine_Query_Token::T_NOT); + $this->_not = true; + } + + $this->_parser->match(Doctrine_Query_Token::T_LIKE); + + $this->_expression = $this->AST('Expression', $paramHolder); + + if ($this->_isNextToken(Doctrine_Query_Token::T_ESCAPE)) { + $this->_parser->match(Doctrine_Query_Token::T_ESCAPE); + $this->_parser->match(Doctrine_Query_Token::T_STRING); + + $this->_escapeString = $this->_parser->token['value']; + } + } + + + public function buildSql() + { + return (($this->_not) ? 'NOT ' : '') . 'LIKE ' . $this->_expression->buildSql() + . (($this->_escapeString !== null) ? ' ESCAPE ' . $this->_escapeString : ''); + } +} diff --git a/lib/Doctrine/Query/Production/LimitClause.php b/lib/Doctrine/Query/Production/LimitClause.php new file mode 100644 index 000000000..aa6bc6487 --- /dev/null +++ b/lib/Doctrine/Query/Production/LimitClause.php @@ -0,0 +1,48 @@ +. + */ + +/** + * LimitClause = "LIMIT" integer + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_LimitClause extends Doctrine_Query_Production +{ + protected $_limit; + + + public function execute(array $params = array()) + { + $this->_parser->match(Doctrine_Query_Token::T_LIMIT); + + $this->_parser->match(Doctrine_Query_Token::T_INTEGER); + $this->_limit = $this->_parser->token['value']; + + return $this; + } +} diff --git a/lib/Doctrine/Query/Production/NullComparisonExpression.php b/lib/Doctrine/Query/Production/NullComparisonExpression.php new file mode 100755 index 000000000..10ee9c099 --- /dev/null +++ b/lib/Doctrine/Query/Production/NullComparisonExpression.php @@ -0,0 +1,58 @@ +. + */ + +/** + * NullComparisonExpression = "IS" ["NOT"] "NULL" + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_NullComparisonExpression extends Doctrine_Query_Production +{ + protected $_not; + + + public function syntax($paramHolder) + { + $this->_not = false; + + $this->_parser->match(Doctrine_Query_Token::T_IS); + + if ($this->_isNextToken(Doctrine_Query_Token::T_NOT)) { + $this->_parser->match(Doctrine_Query_Token::T_NOT); + $this->_not = true; + } + + $this->_parser->match(Doctrine_Query_Token::T_NULL); + } + + + public function buildSql() + { + return 'IS ' . (($this->_not) ? 'NOT ' : '') . 'NULL'; + } +} diff --git a/lib/Doctrine/Query/Production/OffsetClause.php b/lib/Doctrine/Query/Production/OffsetClause.php new file mode 100644 index 000000000..c7c78d423 --- /dev/null +++ b/lib/Doctrine/Query/Production/OffsetClause.php @@ -0,0 +1,57 @@ +. + */ + +/** + * OffsetClause = "OFFSET" integer + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_OffsetClause extends Doctrine_Query_Production +{ + protected $_offset; + + + public function execute(array $params = array()) + { + $this->_parser->match(Doctrine_Query_Token::T_OFFSET); + + $this->_parser->match(Doctrine_Query_Token::T_INTEGER); + $this->_offset = $this->_parser->token['value']; + + return $this; + } + + + public function buildSql() + { + // [TODO] How to deal with different DBMS here? + // The responsability to apply the limit-subquery is from + // SelectStatement, not this object's one. + return ' OFFSET ' . $this->_offset; + } +} diff --git a/lib/Doctrine/Query/Production/OrderByClause.php b/lib/Doctrine/Query/Production/OrderByClause.php new file mode 100644 index 000000000..3e8cd5565 --- /dev/null +++ b/lib/Doctrine/Query/Production/OrderByClause.php @@ -0,0 +1,68 @@ +. + */ + +/** + * OrderByClause = "ORDER" "BY" OrderByItem {"," OrderByItem} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_OrderByClause extends Doctrine_Query_Production +{ + protected $_orderByItems = array(); + + + public function syntax($paramHolder) + { + $this->_parser->match(Doctrine_Query_Token::T_ORDER); + $this->_parser->match(Doctrine_Query_Token::T_BY); + + $this->_orderByItems[] = $this->AST('OrderByItem', $paramHolder); + + while ($this->_isNextToken(',')) { + $this->_parser->match(','); + $this->_orderByItems[] = $this->AST('OrderByItem', $paramHolder); + } + } + + + public function buildSql() + { + $str = 'ORDER BY '; + + for ($i = 0, $l = count($this->_orderByItems); $i < $l; $i++) { + if ($i != 0) { + $str .= ', '; + } + + $str .= ( $this->_orderByItems[$i] instanceof Doctrine_Query_Production ) ? + $this->_orderByItems[$i]->buildSql() : $this->_orderByItems[$i]; + } + + return $str; + } +} diff --git a/lib/Doctrine/Query/Production/OrderByItem.php b/lib/Doctrine/Query/Production/OrderByItem.php new file mode 100644 index 000000000..e9ea65a7d --- /dev/null +++ b/lib/Doctrine/Query/Production/OrderByItem.php @@ -0,0 +1,59 @@ +. + */ + +/** + * OrderByItem = Expression ["ASC" | "DESC"] + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_OrderByItem extends Doctrine_Query_Production +{ + protected $_expression; + + protected $_orderType; + + + public function syntax($paramHolder) + { + $this->_expression = $this->AST('Expression', $paramHolder); + $this->_orderType = 'ASC'; + + if ($this->_isNextToken(Doctrine_Query_Token::T_ASC)) { + $this->_parser->match(Doctrine_Query_Token::T_ASC); + } elseif ($this->_isNextToken(Doctrine_Query_Token::T_DESC)) { + $this->_parser->match(Doctrine_Query_Token::T_DESC); + $this->_orderType = 'DESC'; + } + } + + + public function buildSql() + { + return $this->_expression->buildSql() . ' ' . $this->_orderType; + } +} diff --git a/lib/Doctrine/Query/Production/PathExpression.php b/lib/Doctrine/Query/Production/PathExpression.php new file mode 100644 index 000000000..4d9f9a457 --- /dev/null +++ b/lib/Doctrine/Query/Production/PathExpression.php @@ -0,0 +1,148 @@ +. + */ + +/** + * PathExpression = identifier { "." identifier } + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_PathExpression extends Doctrine_Query_Production +{ + protected $_identifiers = array(); + + + public function syntax($paramHolder) + { + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + $this->_identifiers[] = $this->_parser->token['value']; + + while ($this->_isNextToken('.')) { + $this->_parser->match('.'); + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + + $this->_identifiers[] = $this->_parser->token['value']; + } + } + + + public function semantical($paramHolder) + { + $parserResult = $this->_parser->getParserResult(); + $classMetadata = null; + + for ($i = 0, $l = count($this->_identifiers); $i < $l; $i++) { + if ($i < $l - 1) { + $relationName = $this->_identifiers[$i]; + + // We are still checking for relations + if ( $classMetadata !== null && ! $classMetadata->hasRelation($relationName)) { + $className = $classMetadata->getClassName(); + + $this->_parser->semanticalError("Relation '{$relationName}' does not exist in component '{$className}'"); + + // Assigning new ClassMetadata + $classMetadata = $classMetadata->getRelation($relationName)->getClassMetadata(); + } elseif ( $classMetadata === null ) { + $queryComponent = $parserResult->getQueryComponent($relationName); + + // We should have a semantical error if the queryComponent does not exists yet + if ($queryComponent === null) { + $this->_parser->semanticalError("Undefined component alias for relation '{$relationName}'"); + } + + // Initializing ClassMetadata + $classMetadata = $queryComponent['metadata']; + } + } else { + $fieldName = $this->_identifiers[$i]; + + // We are checking for fields + if ($classMetadata === null) { + // No metadata selection until now. We might need to deal with: + // DELETE FROM Obj alias WHERE field = X + $queryComponents = $parserResult->getQueryComponents(); + + // Check if we have more than one queryComponent defined + if (count($queryComponents) != 1) { + $this->_parser->semanticalError("Undefined component alias for field '{$fieldName}'"); + } + + // Retrieve ClassMetadata + $k = array_keys($queryComponents); + $componentAlias = $k[0]; + + $classMetadata = $queryComponents[$componentAlias]['metadata']; + array_unshift($this->_identifiers, $componentAlias); + } + + // Check if field exists in ClassMetadata + if ( ! $classMetadata->hasField($fieldName)) { + $className = $classMetadata->getClassName(); + + $this->_parser->semanticalError("Field '{$fieldName}' does not exist in component '{$className}'"); + } + } + } + } + + + public function buildSql() + { + // Basic handy variables + $parserResult = $this->_parser->getParserResult(); + + // Retrieving connection + $conn = $this->_parser->getSqlBuilder()->getConnection(); + $manager = Doctrine_Manager::getInstance(); + + // _identifiers are always >= 2 + if ($manager->hasConnectionForComponent($this->_identifiers[0])) { + $conn = $manager->getConnectionForComponent($this->_identifiers[0]); + } + + $str = ''; + + for ($i = 0, $l = count($this->_identifiers); $i < $l; $i++) { + if ($i < $l - 1) { + // [TODO] We are assuming we never define relations in SELECT + // and WHERE clauses. So, do not bother about table alias that + // may not be previously added. At a later stage, we should + // deal with it too. + $str .= $parserResult->getTableAliasFromComponentAlias($this->_identifiers[$i]) . '.'; + } else { + // Retrieving last ClassMetadata + $queryComponent = $parserResult->getQueryComponent($this->_identifiers[$i - 1]); + $classMetadata = $queryComponent['metadata']; + + $str .= $classMetadata->getColumnName($this->_identifiers[$i]); + } + } + + return $conn->quoteIdentifier($str); + } +} diff --git a/lib/Doctrine/Query/Production/PathExpressionEndingWithAsterisk.php b/lib/Doctrine/Query/Production/PathExpressionEndingWithAsterisk.php new file mode 100644 index 000000000..637eb062a --- /dev/null +++ b/lib/Doctrine/Query/Production/PathExpressionEndingWithAsterisk.php @@ -0,0 +1,106 @@ +. + */ + +/** + * PathExpressionEndingWithAsterisk = {identifier "."} "*" + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_PathExpressionEndingWithAsterisk extends Doctrine_Query_Production +{ + protected $_identifiers = array(); + + + public function syntax($paramHolder) + { + // PathExpressionEndingWithAsterisk = {identifier "."} "*" + while ($this->_isNextToken(Doctrine_Query_Token::T_IDENTIFIER)) { + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + $this->_identifiers[] = $this->_parser->token['value']; + + $this->_parser->match('.'); + } + + $this->_parser->match('*'); + } + + + public function semantical($paramHolder) + { + $parserResult = $this->_parser->getParserResult(); + + if (($l = count($this->_identifiers)) > 0) { + // We are dealing with component{.component}.* + $classMetadata = null; + + for ($i = 0; $i < $l; $i++) { + $relationName = $this->_identifiers[$i]; + + // We are still checking for relations + if ( $classMetadata !== null && ! $classMetadata->hasRelation($relationName)) { + $className = $classMetadata->getClassName(); + + $this->_parser->semanticalError("Relation '{$relationName}' does not exist in component '{$className}'"); + + // Assigning new ClassMetadata + $classMetadata = $classMetadata->getRelation($relationName)->getClassMetadata(); + } elseif ( $classMetadata === null ) { + $queryComponent = $parserResult->getQueryComponent($relationName); + + // We should have a semantical error if the queryComponent does not exists yet + if ($queryComponent === null) { + $this->_parser->semanticalError("Undefined component alias for relation '{$relationName}'"); + } + + // Initializing ClassMetadata + $classMetadata = $queryComponent['metadata']; + } + } + } else { + // We are dealing with a simple * as our PathExpression. + // We need to check if there's only one query component. + $queryComponents = $parserResult->getQueryComponents(); + + if (count($queryComponents) != 1) { + $this->_parser->semanticalError( + "Cannot use * as selector expression for multiple components." + ); + } + + // We simplify our life adding the component alias to our AST, + // since we have it on hands now. + $k = array_keys($queryComponents); + $this->_identifiers[] = $k[0]; + } + } + + + public function buildSql() + { + return ''; + } +} diff --git a/lib/Doctrine/Query/Production/Primary.php b/lib/Doctrine/Query/Production/Primary.php new file mode 100644 index 000000000..7011d9790 --- /dev/null +++ b/lib/Doctrine/Query/Production/Primary.php @@ -0,0 +1,85 @@ +. + */ + +/** + * Primary = PathExpression | Atom | "(" Expression ")" | Function | AggregateExpression + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_Primary extends Doctrine_Query_Production +{ + protected $_expression; + + + public function syntax($paramHolder) + { + // Primary = PathExpression | Atom | "(" Expression ")" | Function | AggregateExpression + switch ($this->_parser->lookahead['type']) { + case Doctrine_Query_Token::T_IDENTIFIER: + if ($this->_isFunction()) { + return $this->AST('Function', $paramHolder); + } else { + return $this->AST('PathExpression', $paramHolder); + } + break; + + case Doctrine_Query_Token::T_STRING: + case Doctrine_Query_Token::T_INTEGER: + case Doctrine_Query_Token::T_FLOAT: + case Doctrine_Query_Token::T_INPUT_PARAMETER: + return $this->AST('Atom', $paramHolder); + break; + + case Doctrine_Query_Token::T_AVG: + case Doctrine_Query_Token::T_COUNT: + case Doctrine_Query_Token::T_MAX: + case Doctrine_Query_Token::T_MIN: + case Doctrine_Query_Token::T_SUM: + return $this->AST('AggregateExpression', $paramHolder); + break; + + case Doctrine_Query_Token::T_NONE: + if ($this->_isNextToken('(')) { + $this->_parser->match('('); + $this->_expression = $this->AST('Expression', $paramHolder); + $this->_parser->match(')'); + } + break; + + default: + $this->_parser->syntaxError('Could not process primary type'); + break; + } + } + + + public function buildSql() + { + return '(' . $this->_expression->buildSql() . ')'; + } +} diff --git a/lib/Doctrine/Query/Production/QuantifiedExpression.php b/lib/Doctrine/Query/Production/QuantifiedExpression.php new file mode 100644 index 000000000..44bf66b0b --- /dev/null +++ b/lib/Doctrine/Query/Production/QuantifiedExpression.php @@ -0,0 +1,72 @@ +. + */ + +/** + * QuantifiedExpression = ("ALL" | "ANY" | "SOME") "(" Subselect ")" + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_QuantifiedExpression extends Doctrine_Query_Production +{ + protected $_type; + + protected $_subselect; + + + public function syntax($paramHolder) + { + switch ($this->_parser->lookahead['type']) { + case Doctrine_Query_Token::T_ALL: + $this->_parser->match(Doctrine_Query_Token::T_ALL); + break; + + case Doctrine_Query_Token::T_ANY: + $this->_parser->match(Doctrine_Query_Token::T_ANY); + break; + + case Doctrine_Query_Token::T_SOME: + $this->_parser->match(Doctrine_Query_Token::T_SOME); + break; + + default: + $this->_parser->logError('ALL, ANY or SOME'); + break; + } + + $this->_type = strtoupper($this->_parser->lookahead['value']); + + $this->_parser->match('('); + $this->_subselect = $this->AST('Subselect', $paramHolder); + $this->_parser->match(')'); + } + + + public function buildSql() + { + return $this->_type . ' (' . $this->_subselect->buildSql() . ')'; + } +} diff --git a/lib/Doctrine/Query/Production/QueryLanguage.php b/lib/Doctrine/Query/Production/QueryLanguage.php new file mode 100644 index 000000000..0f507295b --- /dev/null +++ b/lib/Doctrine/Query/Production/QueryLanguage.php @@ -0,0 +1,57 @@ +. + */ + +/** + * QueryLanguage = SelectStatement | UpdateStatement | DeleteStatement + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_QueryLanguage extends Doctrine_Query_Production +{ + public function syntax($paramHolder) + { + // QueryLanguage = SelectStatement | UpdateStatement | DeleteStatement + switch ($this->_parser->lookahead['type']) { + case Doctrine_Query_Token::T_SELECT: + return $this->AST('SelectStatement', $paramHolder); + break; + + case Doctrine_Query_Token::T_UPDATE: + return $this->AST('UpdateStatement', $paramHolder); + break; + + case Doctrine_Query_Token::T_DELETE: + return $this->AST('DeleteStatement', $paramHolder); + break; + + default: + $this->_parser->syntaxError('SELECT, UPDATE or DELETE'); + break; + } + } +} diff --git a/lib/Doctrine/Query/Production/RangeVariableDeclaration.php b/lib/Doctrine/Query/Production/RangeVariableDeclaration.php new file mode 100644 index 000000000..700aa47c7 --- /dev/null +++ b/lib/Doctrine/Query/Production/RangeVariableDeclaration.php @@ -0,0 +1,237 @@ +. + */ + +/** + * RangeVariableDeclaration = identifier {"." identifier} [["AS"] IdentificationVariable] + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_RangeVariableDeclaration extends Doctrine_Query_Production +{ + protected $_identifiers = array(); + + protected $_identificationVariable; + + + public function syntax($paramHolder) + { + // RangeVariableDeclaration = identifier {"." identifier} [["AS"] IdentificationVariable] + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + $this->_identifiers[] = $this->_parser->token['value']; + + while ($this->_isNextToken('.')) { + $this->_parser->match('.'); + $this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER); + + $this->_identifiers[] = $this->_parser->token['value']; + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_AS)) { + $this->_parser->match(Doctrine_Query_Token::T_AS); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_IDENTIFIER)) { + $paramHolder->set('componentName', implode('.', $this->_identifiers)); + + // Will return an identifier, with the semantical check already applied + $this->_identificationVariable = $this->AST('IdentificationVariable', $paramHolder); + + $paramHolder->remove('componentName'); + } + } + + + public function semantical($paramHolder) + { + $parserResult = $this->_parser->getParserResult(); + $componentName = implode('.', $this->_identifiers); + + if ($parserResult->hasQueryComponent($componentName)) { + //echo "Entered in if of hasQueryComponent(".$componentName."): true\n"; + + // As long as name != alias, try to bring the queryComponent from name (already processed) + $queryComponent = $parserResult->getQueryComponent($componentName); + + // Check if we defined _identificationVariable. We throw semantical error if not + if ($this->_identificationVariable === null) { + $componentName = $queryComponent['metadata']->getClassName(); + + $this->_parser->semanticalError( + "Cannot re-declare component '{$componentName}'. Please assign an alias to it." + ); + + return; + } + } else { + //echo "Entered in if hasQueryComponent(".$componentName."), alias ".var_export($this->_identificationVariable, true).": false\n"; + + // No queryComponent was found. We will have to build it for the first time + if (count($this->_identifiers) > 1) { + // We are in a multiple identifier declaration; we are dealing with relations here + $this->_semanticalWithMultipleIdentifier(); + } else { + // We are in a single identifier declaration; our identifier is the class name + $this->_semanticalWithSingleIdentifier(); + } + } + + return $this->_identificationVariable; + } + + + public function buildSql() + { + return ''; + } + + + private function _semanticalWithSingleIdentifier() + { + $parserResult = $this->_parser->getParserResult(); + + // Get the connection for the component + $conn = $this->_parser->getSqlBuilder()->getConnection(); + $manager = Doctrine_EntityManager::getManager(); + $componentName = $this->_identifiers[0]; + + // Retrieving ClassMetadata and Mapper + try { + $classMetadata = $manager->getClassMetadata($componentName); + + // Building queryComponent + $queryComponent = array( + 'metadata' => $classMetadata, + 'mapper' => $manager->getEntityPersister($componentName), + 'parent' => null, + 'relation' => null, + 'map' => null, + 'agg' => null, + ); + } catch (Doctrine_Exception $e) { + //echo "Tried to load class metadata from '".$componentName."': " . $e->getMessage() . "\n"; + $this->_parser->semanticalError($e->getMessage()); + + return; + } + + if ($this->_identificationVariable === null) { + $this->_identificationVariable = $componentName; + } + + //echo "Identification Variable: " .$this->_identificationVariable . "\n"; + + $tableAlias = $parserResult->generateTableAlias($componentName); + $parserResult->setQueryComponent($this->_identificationVariable, $queryComponent); + $parserResult->setTableAlias($tableAlias, $this->_identificationVariable); + } + + + private function _semanticalWithMultipleIdentifier() + { + $parserResult = $this->_parser->getParserResult(); + + // Get the connection for the component + $conn = $this->_parser->getSqlBuilder()->getConnection(); + $manager = Doctrine_EntityManager::getManager(); + + // Retrieve the base component + try { + $queryComponent = $parserResult->getQueryComponent($this->_identifiers[0]); + $classMetadata = $queryComponent['metadata']; + $className = $classMetadata->getClassName(); + $parent = $path = $this->_identifiers[0]; + } catch (Doctrine_Exception $e) { + $this->_parser->semanticalError($e->getMessage()); + + return; + } + + // We loop into others identifier to build query components + for ($i = 1, $l = count($this->_identifiers); $i < $l; $i++) { + $relationName = $this->_identifiers[$i]; + $path = '.' . $relationName; + + if ($parserResult->hasQueryComponent($path)) { + // We already have the query component on hands, get it + $queryComponent = $parserResult->getQueryComponent($path); + $classMetadata = $queryComponent['metadata']; + + // If we are in our last check and identification variable is null, we throw semantical error + if ($i == $l - 1 && $this->_identificationVariable === null) { + $componentName = $classMetadata->getClassName(); + + $this->_parser->semanticalError( + "Cannot re-declare component '{$componentName}' in path '{$path}'. " . + "Please assign an alias to it." + ); + + return; + } + } else { + // We don't have the query component yet + if ( ! $classMetadata->hasRelation($relationName)) { + $className = $classMetadata->getClassName(); + + $this->_parser->semanticalError("Relation '{$relationName}' does not exist in component '{$className}'"); + + return; + } + + // Retrieving ClassMetadata and Mapper + try { + $relation = $classMetadata->getRelation($relationName); + $classMetadata = $relation->getClassMetadata(); + + $queryComponent = array( + 'metadata' => $classMetadata, + 'mapper' => $manager->getEntityPersister($relation->getForeignComponentName()), + 'parent' => $parent, + 'relation' => $relation, + 'map' => null, + 'agg' => null, + ); + + $parent = $path; + } catch (Doctrine_Exception $e) { + //echo "Tried to load class metadata from '".$relationName."'\n"; + $this->_parser->semanticalError($e->getMessage()); + + return; + } + } + } + + if ($this->_identificationVariable === null) { + $this->_identificationVariable = $path; + } + + $tableAlias = $parserResult->generateTableAlias($path); + $parserResult->setQueryComponent($this->_identificationVariable, $queryComponent); + $parserResult->setTableAlias($tableAlias, $this->_identificationVariable); + } +} diff --git a/lib/Doctrine/Query/Production/SelectClause.php b/lib/Doctrine/Query/Production/SelectClause.php new file mode 100644 index 000000000..f40c4475d --- /dev/null +++ b/lib/Doctrine/Query/Production/SelectClause.php @@ -0,0 +1,88 @@ +. + */ + +/** + * SelectClause = "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_SelectClause extends Doctrine_Query_Production +{ + protected $_isDistinct; + + protected $_selectExpressions = array(); + + + public function syntax($paramHolder) + { + // SelectClause = "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} + $this->_isDistinct = false; + + $this->_parser->match(Doctrine_Query_Token::T_SELECT); + + if ($this->_isNextToken(Doctrine_Query_Token::T_DISTINCT)) { + $this->_parser->match(Doctrine_Query_Token::T_DISTINCT); + $this->_isDistinct = true; + } + + $this->_selectExpressions[] = $this->AST('SelectExpression', $paramHolder); + + while ($this->_isNextToken(',')) { + $this->_parser->match(','); + $this->_selectExpressions[] = $this->AST('SelectExpression', $paramHolder); + } + } + + + public function semantical($paramHolder) + { + // We need to validate each SelectExpression + for ($i = 0, $l = count($this->_selectExpressions); $i < $l; $i++) { + $this->_selectExpressions[$i]->semantical($paramHolder); + } + } + + + public function buildSql() + { + return 'SELECT ' . (($this->_isDistinct) ? 'DISTINCT ' : '') + . implode(', ', $this->_mapSelectExpressions()); + } + + + protected function _mapSelectExpressions() + { + return array_map(array(&$this, '_mapSelectExpression'), $this->_selectExpressions); + } + + + protected function _mapSelectExpression($value) + { + return $value->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/SelectExpression.php b/lib/Doctrine/Query/Production/SelectExpression.php new file mode 100644 index 000000000..d5a9668a6 --- /dev/null +++ b/lib/Doctrine/Query/Production/SelectExpression.php @@ -0,0 +1,126 @@ +. + */ + +/** + * SelectExpression = (PathExpressionEndingWithAsterisk | Expression | "(" Subselect ")") + * [["AS"] IdentificationVariable] + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_SelectExpression extends Doctrine_Query_Production +{ + protected $_leftExpression; + + protected $_isSubselect; + + protected $_identificationVariable; + + private $__columnAliasInSql; + + + public function syntax($paramHolder) + { + // SelectExpression = (PathExpressionEndingWithAsterisk | Expression | "(" Subselect ")") + // [["AS"] IdentificationVariable] + $this->_isSubselect = false; + + if ($this->_isPathExpressionEndingWithAsterisk()) { + $this->_leftExpression = $this->AST('PathExpressionEndingWithAsterisk', $paramHolder); + } elseif(($this->_isSubselect = $this->_isSubselect()) === true) { + $this->_parser->match('('); + $this->_leftExpression = $this->AST('Subselect', $paramHolder); + $this->_parser->match(')'); + } else { + $this->_leftExpression = $this->AST('Expression', $paramHolder); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_AS)) { + $this->_parser->match(Doctrine_Query_Token::T_AS); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_IDENTIFIER)) { + $this->_identificationVariable = $this->AST('IdentificationVariable', $paramHolder); + } + } + + + public function semantical($paramHolder) + { + // Here we inspect for duplicate IdentificationVariable, and if the + // left expression needs the identification variable. If yes, check + // its existance. + if ($this->_leftExpression instanceof Doctrine_Query_Production_PathExpressionEndingWithAsterisk && $this->_identificationVariable !== null) { + $this->_parser->semanticalError( + "Cannot assign an identification variable to a path expression with asterisk (ie. foo.bar.* AS foobaz)." + ); + } + + if ($this->_identificationVariable !== null) { + if ($this->_leftExpression instanceof Doctrine_Query_Production_PathExpression) { + // We bring the queryComponent from the class instance + // $queryComponent = $this->_leftExpression->getQueryComponent(); + } else { + // We bring the default queryComponent + // $queryComponent = $parserResult->getQueryComponent(null); + } + + $idx = count($queryComponent['scalar']); + $this->__columnAliasInSql .= '__' . $idx; + + $queryComponent['scalar'][$idx] = $this->_identificationVariable; + + //$parserResult->setQueryComponent($componentAlias, $queryComponent); + } + + // We need to add scalar in queryComponent the item alias if identificationvariable is set. + echo "SelectExpression:\n"; + echo get_class($this->_leftExpression) . "\n"; + + // The check for duplicate IdentificationVariable was already done + } + + + public function buildSql() + { + return $this->_leftExpression->buildSql() . ' AS ' + . (($this->_identificationVariable !== null) ? $this->_identificationVariable : ''); + } + + + protected function _isPathExpressionEndingWithAsterisk() + { + $token = $this->_parser->lookahead; + $this->_parser->getScanner()->resetPeek(); + + while (($token['type'] === Doctrine_Query_Token::T_IDENTIFIER) || ($token['value'] === '.')) { + $token = $this->_parser->getScanner()->peek(); + } + + return $token['value'] === '*'; + } +} diff --git a/lib/Doctrine/Query/Production/SelectStatement.php b/lib/Doctrine/Query/Production/SelectStatement.php new file mode 100644 index 000000000..bba523c5b --- /dev/null +++ b/lib/Doctrine/Query/Production/SelectStatement.php @@ -0,0 +1,96 @@ +. + */ + +/** + * SelectStatement = SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_SelectStatement extends Doctrine_Query_Production +{ + protected $_selectClause; + + protected $_fromClause; + + protected $_whereClause; + + protected $_groupByClause; + + protected $_havingClause; + + protected $_orderByClause; + + + public function syntax($paramHolder) + { + // SelectStatement = SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] + + // Disable the semantical check for SelectClause now. This is needed + // since we dont know the query components yet (will be known only + // when the FROM clause be processed). + $paramHolder->set('semanticalCheck', false); + $this->_selectClause = $this->AST('SelectClause', $paramHolder); + $paramHolder->remove('semanticalCheck'); + + $this->_fromClause = $this->AST('FromClause', $paramHolder); + + if ($this->_isNextToken(Doctrine_Query_Token::T_WHERE)) { + $this->_whereClause = $this->AST('WhereClause', $paramHolder); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_GROUP)) { + $this->_groupByClause = $this->AST('GroupByClause', $paramHolder); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_HAVING)) { + $this->_havingClause = $this->AST('HavingClause', $paramHolder); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_ORDER)) { + $this->_orderByClause = $this->AST('OrderByClause', $paramHolder); + } + } + + + public function semantical($paramHolder) + { + // We need to invoke the semantical check of SelectClause here, since + // it was not yet checked. + $this->_selectClause->semantical($paramHolder); + } + + + public function buildSql() + { + return $this->_selectClause->buildSql() . ' ' . $this->_fromClause->buildSql() + . (($this->_whereClause !== null) ? ' ' . $this->_whereClause->buildSql() : ' WHERE 1 = 1') + . (($this->_groupByClause !== null) ? ' ' . $this->_groupByClause->buildSql() : '') + . (($this->_havingClause !== null) ? ' ' . $this->_havingClause->buildSql() : '') + . (($this->_orderByClause !== null) ? ' ' . $this->_orderByClause->buildSql() : ''); + } +} diff --git a/lib/Doctrine/Query/Production/SimpleConditionalExpression.php b/lib/Doctrine/Query/Production/SimpleConditionalExpression.php new file mode 100644 index 000000000..35c484e8e --- /dev/null +++ b/lib/Doctrine/Query/Production/SimpleConditionalExpression.php @@ -0,0 +1,109 @@ +. + */ + +/** + * SimpleConditionalExpression = + * ExistsExpression | Expression (ComparisonExpression | BetweenExpression | + * LikeExpression | InExpression | NullComparisonExpression | QuantifiedExpression) + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_SimpleConditionalExpression extends Doctrine_Query_Production +{ + protected $_leftExpression; + + protected $_rightExpression; + + + public function syntax($paramHolder) + { + // SimpleConditionalExpression = + // ExistsExpression | Expression (ComparisonExpression | BetweenExpression | + // LikeExpression | InExpression | NullComparisonExpression | QuantifiedExpression) + if ($this->_getExpressionType() === Doctrine_Query_Token::T_EXISTS) { + return $this->AST('ExistsExpression', $paramHolder); + } + + $this->_leftExpression = $this->AST('Expression', $paramHolder); + + switch ($this->_getExpressionType()) { + case Doctrine_Query_Token::T_BETWEEN: + $this->_rightExpression = $this->AST('BetweenExpression', $paramHolder); + break; + + case Doctrine_Query_Token::T_LIKE: + $this->_rightExpression = $this->AST('LikeExpression', $paramHolder); + break; + + case Doctrine_Query_Token::T_IN: + $this->_rightExpression = $this->AST('InExpression', $paramHolder); + break; + + case Doctrine_Query_Token::T_IS: + $this->_rightExpression = $this->AST('NullComparisonExpression', $paramHolder); + break; + + case Doctrine_Query_Token::T_ALL: + case Doctrine_Query_Token::T_ANY: + case Doctrine_Query_Token::T_SOME: + $this->_rightExpression = $this->AST('QuantifiedExpression', $paramHolder); + break; + + case Doctrine_Query_Token::T_NONE: + // [TODO] Check out ticket #935 to understand what will be done with enumParams + $this->_rightExpression = $this->AST('ComparisonExpression', $paramHolder); + break; + + default: + $message = "BETWEEN, LIKE, IN, IS, quantified (ALL, ANY or SOME) " + . "or comparison (=, <, <=, <>, >, >=, !=)"; + $this->_parser->syntaxError($message); + break; + } + } + + + public function buildSql() + { + return $this->_leftExpression->buildSql() . ' ' . $this->_rightExpression->buildSql(); + } + + + protected function _getExpressionType() { + if ($this->_isNextToken(Doctrine_Query_Token::T_NOT)) { + $scanner = $this->_parser->getScanner(); + + $token = $scanner->peek(); + $scanner->resetPeek(); + } else { + $token = $this->_parser->lookahead; + } + + return $token['type']; + } +} diff --git a/lib/Doctrine/Query/Production/SimpleSelectClause.php b/lib/Doctrine/Query/Production/SimpleSelectClause.php new file mode 100644 index 000000000..017aa9400 --- /dev/null +++ b/lib/Doctrine/Query/Production/SimpleSelectClause.php @@ -0,0 +1,68 @@ +. + */ + +/** + * SimpleSelectClause = "SELECT" ["DISTINCT"] SelectExpression + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_SimpleSelectClause extends Doctrine_Query_Production +{ + protected $_isDistinct; + + protected $_selectExpression; + + + public function syntax($paramHolder) + { + // SimpleSelectClause = "SELECT" ["DISTINCT"] SelectExpression + $this->_isDistinct = false; + + $this->_parser->match(Doctrine_Query_Token::T_SELECT); + + if ($this->_isNextToken(Doctrine_Query_Token::T_DISTINCT)) { + $this->_parser->match(Doctrine_Query_Token::T_DISTINCT); + $this->_isDistinct = true; + } + + $this->_selectExpression = $this->AST('SelectExpression', $paramHolder); + } + + + public function semantical($paramHolder) + { + // We need to validate the SelectExpression + $this->_selectExpression->semantical($paramHolder); + } + + + public function buildSql() + { + return 'SELECT ' . (($this->_isDistinct) ? 'DISTINCT ' : '') + . $this->_selectExpression->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/Subselect.php b/lib/Doctrine/Query/Production/Subselect.php new file mode 100644 index 000000000..4015b9d26 --- /dev/null +++ b/lib/Doctrine/Query/Production/Subselect.php @@ -0,0 +1,105 @@ +. + */ + +/** + * Subselect = SimpleSelectClause FromClause [WhereClause] [GroupByClause] + * [HavingClause] [OrderByClause] [LimitClause] [OffsetClause] + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_Subselect extends Doctrine_Query_Production +{ + protected $_simpleSelectClause; + + protected $_fromClause; + + protected $_whereClause; + + protected $_groupByClause; + + protected $_havingClause; + + protected $_orderByClause; + + protected $_limitClause; + + + public function syntax($paramHolder) + { + // Subselect = SimpleSelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] [LimitClause] + + // Disable the semantical check for SelectClause now. This is needed + // since we dont know the query components yet (will be known only + // when the FROM clause be processed). + $paramHolder->set('semanticalCheck', false); + $this->_simpleSelectClause = $this->AST('SimpleSelectClause', $paramHolder); + $paramHolder->remove('semanticalCheck'); + + $this->_fromClause = $this->AST('FromClause', $paramHolder); + + if ($this->_isNextToken(Doctrine_Query_Token::T_WHERE)) { + $this->_whereClause = $this->AST('WhereClause', $paramHolder); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_GROUP)) { + $this->_groupByClause = $this->AST('GroupByClause', $paramHolder); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_HAVING)) { + $this->_havingClause = $this->AST('HavingClause', $paramHolder); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_ORDER)) { + $this->_orderByClause = $this->AST('OrderByClause', $paramHolder); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_LIMIT)) { + $this->_limitClause = $this->AST('LimitClause', $paramHolder); + } + + } + + + public function semantical($paramHolder) + { + // We need to invoke the semantical check of SelectClause here, since + // it was not yet checked. + $this->_simpleSelectClause->semantical($paramHolder); + } + + + public function buildSql() + { + return $this->_simpleSelectClause->buildSql() . ' ' . $this->_fromClause->buildSql() + . (($this->_whereClause !== null) ? ' ' . $this->_whereClause->buildSql() : '') + . (($this->_groupByClause !== null) ? ' ' . $this->_groupByClause->buildSql() : '') + . (($this->_havingClause !== null) ? ' ' . $this->_havingClause->buildSql() : '') + . (($this->_orderByClause !== null) ? ' ' . $this->_orderByClause->buildSql() : ''); + } + +} diff --git a/lib/Doctrine/Query/Production/Term.php b/lib/Doctrine/Query/Production/Term.php new file mode 100644 index 000000000..fcf7f5523 --- /dev/null +++ b/lib/Doctrine/Query/Production/Term.php @@ -0,0 +1,79 @@ +. + */ + +/** + * Term = Factor {("*" | "/") Factor} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_Term extends Doctrine_Query_Production +{ + protected $_factors = array(); + + + public function syntax($paramHolder) + { + // Term = Factor {("*" | "/") Factor} + $this->_factors[] = $this->AST('Factor', $paramHolder); + + while ($this->_isNextToken('*') || $this->_isNextToken('/')) { + if ($this->_isNextToken('*')) { + $this->_parser->match('*'); + $this->_factors[] = '*'; + } else { + $this->_parser->match('/'); + $this->_factors[] = '/'; + } + + $this->_factors[] = $this->AST('Factor', $paramHolder); + } + + // Optimize depth instances in AST + if (count($this->_factors) == 1) { + return $this->_factors[0]; + } + } + + + public function buildSql() + { + return implode(' ', $this->_mapFactors()); + } + + + protected function _mapFactors() + { + return array_map(array(&$this, '_mapFactor'), $this->_factors); + } + + + protected function _mapFactor($value) + { + return (is_string($value) ? $value : $value->buildSql()); + } +} diff --git a/lib/Doctrine/Query/Production/UpdateClause.php b/lib/Doctrine/Query/Production/UpdateClause.php new file mode 100644 index 000000000..e9561e030 --- /dev/null +++ b/lib/Doctrine/Query/Production/UpdateClause.php @@ -0,0 +1,77 @@ +. + */ + +/** + * UpdateClause = "UPDATE" VariableDeclaration "SET" UpdateItem {"," UpdateItem} + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_UpdateClause extends Doctrine_Query_Production +{ + protected $_variableDeclaration; + + protected $_updateItems = array(); + + + public function syntax($paramHolder) + { + // UpdateClause = "UPDATE" VariableDeclaration "SET" UpdateItem {"," UpdateItem} + $this->_parser->match(Doctrine_Query_Token::T_UPDATE); + + $this->_variableDeclaration = $this->AST('VariableDeclaration', $paramHolder); + + $this->_parser->match(Doctrine_Query_Token::T_SET); + + $this->_updateItems[] = $this->AST('UpdateItem', $paramHolder); + + while ($this->_isNextToken(',')) { + $this->_parser->match(','); + + $this->_updateItems[] = $this->AST('UpdateItem', $paramHolder); + } + } + + + public function buildSql() + { + return 'UPDATE ' . $this->_variableDeclaration->buildSql() + . ' SET ' . implode(', ', $this->_mapUpdateItems()); + } + + + protected function _mapUpdateItems() + { + return array_map(array(&$this, '_mapUpdateItem'), $this->_updateItems); + } + + + protected function _mapUpdateItem($value) + { + return $value->buildSql(); + } +} diff --git a/lib/Doctrine/Query/Production/UpdateItem.php b/lib/Doctrine/Query/Production/UpdateItem.php new file mode 100644 index 000000000..a08a43a43 --- /dev/null +++ b/lib/Doctrine/Query/Production/UpdateItem.php @@ -0,0 +1,62 @@ +. + */ + +/** + * UpdateItem = PathExpression "=" (Expression | "NULL") + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_UpdateItem extends Doctrine_Query_Production +{ + protected $_pathExpression; + + protected $_expression; + + + public function syntax($paramHolder) + { + // UpdateItem = PathExpression "=" (Expression | "NULL") + $this->_pathExpression = $this->AST('PathExpression', $paramHolder); + + $this->_parser->match('='); + + if ($this->_isNextToken(Doctrine_Query_Token::T_NULL)) { + $this->_parser->match(Doctrine_Query_Token::T_NULL); + $this->_expression = null; + } else { + $this->_expression = $this->AST('Expression', $paramHolder); + } + } + + + public function buildSql() + { + return $this->_pathExpression->buildSql() . ' = ' + . ($this->_expression === null ? 'NULL' : $this->_expression->buildSql()); + } +} diff --git a/lib/Doctrine/Query/Production/UpdateStatement.php b/lib/Doctrine/Query/Production/UpdateStatement.php new file mode 100644 index 000000000..3da5925f6 --- /dev/null +++ b/lib/Doctrine/Query/Production/UpdateStatement.php @@ -0,0 +1,59 @@ +. + */ + +/** + * UpdateStatement = UpdateClause [WhereClause] + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_UpdateStatement extends Doctrine_Query_Production +{ + protected $_updateClause; + + protected $_whereClause; + + + public function syntax($paramHolder) + { + // UpdateStatement = UpdateClause [WhereClause] + $this->_updateClause = $this->AST('UpdateClause', $paramHolder); + + if ($this->_isNextToken(Doctrine_Query_Token::T_WHERE)) { + $this->_whereClause = $this->AST('WhereClause', $paramHolder); + } + } + + + public function buildSql() + { + // The 1=1 is needed to workaround the affected_rows in MySQL. + // Simple "UPDATE table_name SET column_name = value" gives 0 affected rows. + return $this->_updateClause->buildSql() . (($this->_whereClause !== null) + ? $this->_whereClause->buildSql() : ' WHERE 1 = 1'); + } +} diff --git a/lib/Doctrine/Query/Production/VariableDeclaration.php b/lib/Doctrine/Query/Production/VariableDeclaration.php new file mode 100644 index 000000000..71c26511f --- /dev/null +++ b/lib/Doctrine/Query/Production/VariableDeclaration.php @@ -0,0 +1,138 @@ +. + */ + +/** + * VariableDeclaration = identifier [["AS"] IdentificationVariable] + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_VariableDeclaration extends Doctrine_Query_Production +{ + protected $_componentName; + + protected $_componentAlias; + + + public function syntax($paramHolder) + { + // VariableDeclaration = identifier [["AS"] IdentificationVariable] + if ($this->_parser->match(Doctrine_Query_Token::T_IDENTIFIER)) { + // identifier + $this->_componentName = $this->_parser->token['value']; + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_AS)) { + $this->_parser->match(Doctrine_Query_Token::T_AS); + } + + if ($this->_isNextToken(Doctrine_Query_Token::T_IDENTIFIER)) { + $paramHolder->set('componentName', $this->_componentName); + + // Will return an identifier, with the semantical check already applied + $this->_componentAlias = $this->AST('IdentificationVariable', $paramHolder); + + $paramHolder->remove('componentName'); + } + } + + + public function semantical($paramHolder) + { + $parserResult = $this->_parser->getParserResult(); + + if ($parserResult->hasQueryComponent($this->_componentName)) { + // As long as name != alias, try to bring the queryComponent from name (already processed) + $queryComponent = $parserResult->getQueryComponent($this->_componentName); + + // Check if we defined _componentAlias. We throw semantical error if not + if ($this->_componentAlias === null) { + $componentName = $queryComponent['metadata']->getClassName(); + + $this->_parser->semanticalError( + "Cannot re-declare component '{$this->_componentName}'. Please assign an alias to it." + ); + + return; + } + } else { + // No queryComponent was found. We will have to build it for the first time + + // Get the connection for the component + $conn = $this->_parser->getSqlBuilder()->getConnection(); + $manager = Doctrine_EntityManager::getManager(); + + // Retrieving ClassMetadata and Mapper + try { + $classMetadata = $manager->getMetadata($this->_componentName); + + // Building queryComponent + $queryComponent = array( + 'metadata' => $classMetadata, + 'mapper' => $manager->getEntityPersister($this->_componentName), + 'parent' => null, + 'relation' => null, + 'map' => null, + 'agg' => null, + ); + } catch (Doctrine_Exception $e) { + $this->_parser->semanticalError($e->getMessage()); + + return; + } + } + + // Define ParserResult assertions for later usage + $tableAlias = $this->_parser->getParserResult()->generateTableAlias($this->_componentName); + + if ($this->_componentAlias === null) { + $this->_componentAlias = $this->_componentName; + } + + $parserResult->setQueryComponent($this->_componentAlias, $queryComponent); + $parserResult->setTableAlias($tableAlias, $this->_componentAlias); + } + + + public function buildSql() + { + // Basic handy variables + $parserResult = $this->_parser->getParserResult(); + $queryComponent = $parserResult->getQueryComponent($this->_componentAlias); + + // Retrieving connection + $conn = $this->_parser->getSqlBuilder()->getConnection(); + $manager = Doctrine_Manager::getInstance(); + + if ($manager->hasConnectionForComponent($this->_componentName)) { + $conn = $manager->getConnectionForComponent($this->_componentName); + } + + return $conn->quoteIdentifier($queryComponent['metadata']->getTableName()) . ' ' + . $conn->quoteIdentifier($parserResult->getTableAliasFromComponentAlias($this->_componentAlias)); + } +} diff --git a/lib/Doctrine/Query/Groupby.php b/lib/Doctrine/Query/Production/WhereClause.php similarity index 63% rename from lib/Doctrine/Query/Groupby.php rename to lib/Doctrine/Query/Production/WhereClause.php index f0a84230c..93134987b 100644 --- a/lib/Doctrine/Query/Groupby.php +++ b/lib/Doctrine/Query/Production/WhereClause.php @@ -1,52 +1,52 @@ -. - */ -Doctrine::autoload('Doctrine_Query_Part'); -/** - * Doctrine_Query_Groupby - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - * @author Konsta Vesterinen - */ -class Doctrine_Query_Groupby extends Doctrine_Query_Part -{ - /** - * DQL GROUP BY PARSER - * parses the group by part of the query string - * - * @param string $str - * @return void - */ - public function parse($str, $append = false) - { - $r = array(); - foreach (explode(',', $str) as $reference) { - $reference = trim($reference); - - $r[] = $this->query->parseClause($reference); - } - return implode(', ', $r); - } -} +. + */ + +/** + * WhereClause = "WHERE" ConditionalExpression + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Production_WhereClause extends Doctrine_Query_Production +{ + protected $_conditionalExpression; + + + public function syntax($paramHolder) + { + // WhereClause = "WHERE" ConditionalExpression + $this->_parser->match(Doctrine_Query_Token::T_WHERE); + + $this->_conditionalExpression = $this->AST('ConditionalExpression', $paramHolder); + } + + + public function buildSql() + { + return ' WHERE ' . $this->_conditionalExpression->buildSql(); + } +} diff --git a/lib/Doctrine/Query/ProductionParamHolder.php b/lib/Doctrine/Query/ProductionParamHolder.php new file mode 100644 index 000000000..786dd9069 --- /dev/null +++ b/lib/Doctrine/Query/ProductionParamHolder.php @@ -0,0 +1,89 @@ +. + */ + +/** + * Production variables holder + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_ProductionParamHolder +{ + protected static $_instance; + + protected $_data; + + + protected function __construct() + { + $this->free(); + } + + + public static function create() + { + if ( ! isset(self::$_instance)) { + self::$_instance = new self; + } + + return self::$_instance; + } + + + public function free() + { + $this->_data = array(); + } + + + public function set($offset, $value) + { + $this->_data[$offset] = $value; + } + + + public function get($offset) + { + return isset($this->_data[$offset]) ? $this->_data[$offset] : null; + } + + + public function has($offset) + { + return isset($this->_data[$offset]); + } + + + public function remove($offset) + { + if ($this->has($offset)) { + $this->_data[$offset] = null; + unset($this->_data[$offset]); + } + } +} diff --git a/lib/Doctrine/Query/Orderby.php b/lib/Doctrine/Query/QueryResult.php old mode 100644 new mode 100755 similarity index 65% rename from lib/Doctrine/Query/Orderby.php rename to lib/Doctrine/Query/QueryResult.php index ef9184173..fb09e3474 --- a/lib/Doctrine/Query/Orderby.php +++ b/lib/Doctrine/Query/QueryResult.php @@ -1,53 +1,48 @@ -. - */ -Doctrine::autoload('Doctrine_Query_Part'); -/** - * Doctrine_Query_Orderby - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - * @author Konsta Vesterinen - */ -class Doctrine_Query_Orderby extends Doctrine_Query_Part -{ - /** - * DQL ORDER BY PARSER - * parses the order by part of the query string - * - * @param string $str - * @return void - */ - public function parse($str, $append = false) - { - $ret = array(); - - foreach (explode(',', trim($str)) as $r) { - $r = $this->query->parseClause($r); - - $ret[] = $r; - } - return $ret; - } -} +. + */ + +Doctrine::autoload('Doctrine_Query_AbstractResult'); + +/** + * Doctrine_Query_QueryResult + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_QueryResult extends Doctrine_Query_AbstractResult +{ + /** + * Returns cached resultset. + * + * @return array Resultset. + */ + public function getResultSet() + { + return $this->_data; + } + +} diff --git a/lib/Doctrine/Query/Scanner.php b/lib/Doctrine/Query/Scanner.php new file mode 100644 index 000000000..f4c7bd63a --- /dev/null +++ b/lib/Doctrine/Query/Scanner.php @@ -0,0 +1,219 @@ +. + */ + +/** + * Scans a DQL query for tokens. + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_Scanner +{ + /** + * Array of scanned tokens + * + * @var array + */ + protected $_tokens = array(); + + protected $_position = 0; + + protected $_peek = 0; + + /** + * Creates a new query scanner object. + * + * @param string $input a query string + */ + public function __construct($input) + { + $this->_scan($input); + } + + /** + * Checks if an identifier is a keyword and returns its correct type. + * + * @param string $identifier identifier name + * @return int token type + */ + public function _checkLiteral($identifier) + { + $name = 'Doctrine_Query_Token::T_' . strtoupper($identifier); + + if (defined($name)) { + $type = constant($name); + + if ($type > 100) { + return $type; + } + } + + return Doctrine_Query_Token::T_IDENTIFIER; + } + + /** + * Scans the input string for tokens. + * + * @param string $input a query string + */ + protected function _scan($input) + { + static $regex; + + if ( ! isset($regex)) { + $patterns = array( + '[a-z_][a-z0-9_]*', + '(?:[0-9]+(?:[,\.][0-9]+)*)(?:e[+-]?[0-9]+)?', + "'(?:[^']|'')*'", + '\?|:[a-z]+' + ); + $regex = '/(' . implode(')|(', $patterns) . ')|\s+|(.)/i'; + } + + $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE; + $matches = preg_split($regex, $input, -1, $flags); + + foreach ($matches as $match) { + $value = $match[0]; + + $type = $this->_getType($value); + + $this->_tokens[] = array( + 'value' => $value, + 'type' => $type, + 'position' => $match[1] + ); + } + } + + + protected function _getType(&$value) + { + // $value is referenced because it can be changed if it is numeric. + // [TODO] Revisit the _isNumeric and _getNumeric methods to reduce overhead. + $type = Doctrine_Query_Token::T_NONE; + + $newVal = $this->_getNumeric($value); + if($newVal !== false){ + $value = $newVal; + if (strpos($value, '.') !== false || stripos($value, 'e') !== false) { + $type = Doctrine_Query_Token::T_FLOAT; + } else{ + $type = Doctrine_Query_Token::T_INTEGER; + } + + } + if ($value[0] === "'" && $value[strlen($value) - 1] === "'") { + $type = Doctrine_Query_Token::T_STRING; + } elseif (ctype_alpha($value[0]) || $value[0] === '_') { + $type = $this->_checkLiteral($value); + } elseif ($value[0] === '?' || $value[0] === ':') { + $type = Doctrine_Query_Token::T_INPUT_PARAMETER; + } + + return $type; + } + + + protected function _getNumeric($value) + { + if (!is_scalar($value)) { + return false; + } + // Checking for valid numeric numbers: 1.234, -1.234e-2 + if (is_numeric($value)) { + return $value; + } + + // World number: 1.000.000,02 or -1,234e-2 + $worldnum = strtr($value, array('.' => '', ',' => '.')); + if(is_numeric($worldnum)) { + return $worldnum; + } + + // American extensive number: 1,000,000.02 + $american_en = strtr($value, array(',' => '')); + if (is_numeric($american_en)) { + return $american_en; + } + + return false; + + } + + + public function isA($value, $token) + { + $type = $this->_getType($value); + + return $type === $token; + } + + + public function peek() + { + if (isset($this->_tokens[$this->_position + $this->_peek])) { + return $this->_tokens[$this->_position + $this->_peek++]; + } else { + return null; + } + } + + + public function resetPeek() + { + $this->_peek = 0; + } + + + /** + * Returns the next token in the input string. + * + * A token is an associative array containing three items: + * - 'value' : the string value of the token in the input string + * - 'type' : the type of the token (identifier, numeric, string, input + * parameter, none) + * - 'position' : the position of the token in the input string + * + * @return array|null the next token; null if there is no more tokens left + */ + public function next() + { + $this->_peek = 0; + + if (isset($this->_tokens[$this->_position])) { + return $this->_tokens[$this->_position++]; + } else { + return null; + } + } + + + public function resetPosition($position = 0) + { + $this->_position = $position; + } +} diff --git a/lib/Doctrine/Query/SqlBuilder.php b/lib/Doctrine/Query/SqlBuilder.php new file mode 100755 index 000000000..aea429f38 --- /dev/null +++ b/lib/Doctrine/Query/SqlBuilder.php @@ -0,0 +1,87 @@ +. + */ + +/** + * Base class of each Sql Builder object + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +abstract class Doctrine_Query_SqlBuilder +{ + /** + * The Connection object. + * + * @var Doctrine_Connection + */ + protected $_connection; + + + public static function fromConnection(Doctrine_Connection $connection = null) + { + if ($connection === null) { + $connection = Doctrine_EntityManager::getManager()->getConnection(); + } + + $className = "Doctrine_Query_SqlBuilder_" . $connection->getDriverName(); + $sqlBuilder = new $className(); + $sqlBuilder->_connection = $connection; + + return $sqlBuilder; + } + + + /** + * Retrieves the assocated Doctrine_Connection to this object. + * + * @return Doctrine_Connection + */ + public function getConnection() + { + return $this->_connection; + } + + + /** + * @nodoc + */ + public function quoteIdentifier($identifier) + { + return $this->_connection->quoteIdentifier($identifier); + } + + + + // Start Common SQL generations + // Here we follow the SQL-99 specifications available at: + // http://savage.net.au/SQL/sql-99.bnf + + + + // End of Common SQL generations +} diff --git a/lib/Doctrine/Query/SqlBuilder/Mysql.php b/lib/Doctrine/Query/SqlBuilder/Mysql.php new file mode 100755 index 000000000..6616ebbce --- /dev/null +++ b/lib/Doctrine/Query/SqlBuilder/Mysql.php @@ -0,0 +1,38 @@ +. + */ + +/** + * MySql class of Sql Builder object + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_SqlBuilder_Mysql extends Doctrine_Query_SqlBuilder +{ + +} diff --git a/lib/Doctrine/Query/Limit.php b/lib/Doctrine/Query/SqlBuilder/Sqlite.php old mode 100644 new mode 100755 similarity index 75% rename from lib/Doctrine/Query/Limit.php rename to lib/Doctrine/Query/SqlBuilder/Sqlite.php index f72d07566..acf486675 --- a/lib/Doctrine/Query/Limit.php +++ b/lib/Doctrine/Query/SqlBuilder/Sqlite.php @@ -1,39 +1,38 @@ -. - */ - -/** - * Doctrine_Query_Limit - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision: 1352 $ - * @author Konsta Vesterinen - */ -class Doctrine_Query_Limit extends Doctrine_Query_Part -{ - public function parse($limit) - { - return (int) $limit; - } +. + */ + +/** + * Sqlite class of Sql Builder object + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Doctrine_Query_SqlBuilder_Sqlite extends Doctrine_Query_SqlBuilder +{ + } \ No newline at end of file diff --git a/lib/Doctrine/Query/SqlExecutor/Abstract.php b/lib/Doctrine/Query/SqlExecutor/Abstract.php new file mode 100644 index 000000000..31826af36 --- /dev/null +++ b/lib/Doctrine/Query/SqlExecutor/Abstract.php @@ -0,0 +1,115 @@ +. + */ + +/** + * Doctrine_Query_QueryResult + * + * @package Doctrine + * @subpackage Query + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 2.0 + * @version $Revision$ + */ +abstract class Doctrine_Query_SqlExecutor_Abstract implements Serializable +{ + // [TODO] Remove me later! + public $AST; + + protected $_sqlStatements; + + public function __construct(Doctrine_Query_Production $AST) + { + // [TODO] Remove me later! + $this->AST = $AST; + } + + + /** + * Gets the SQL statements that are executed by the executor. + * + * @return array All the SQL update statements. + */ + public function getSqlStatements() + { + return $this->_sqlStatements; + } + + + /** + * Executes all sql statements. + * + * @param Doctrine_Connection $conn The database connection that is used to execute the queries. + * @param array $params The parameters. + */ + abstract public function execute(Doctrine_Connection $conn, array $params); + + + /** + * Factory method. + * Creates an appropriate sql executor for the given AST. + * + * @param Doctrine_Query_Production $AST The root node of the AST. + * @return Doctrine_Query_SqlExecutor_Abstract The executor that is suitable for the given AST. + */ + public static function create(Doctrine_Query_Production $AST) + { + $isDeleteStatement = $AST instanceof Doctrine_Query_Production_DeleteStatement; + $isUpdateStatement = $AST instanceof Doctrine_Query_Production_UpdateStatement; + + if ($isUpdateStatement || $isDeleteStatement) { + // TODO: Inspect the $AST and create the proper executor like so (pseudo-code): + /* + if (primaryClassInFromClause->isMultiTable()) { + if ($isDeleteStatement) { + return new Doctrine_Query_SqlExecutor_MultiTableDelete($AST); + } else { + return new Doctrine_Query_SqlExecutor_MultiTableUpdate($AST); + } + } else ... + */ + return new Doctrine_Query_SqlExecutor_SingleTableDeleteUpdate($AST); + } else { + return new Doctrine_Query_SqlExecutor_SingleSelect($AST); + } + } + + + /** + * Serializes the sql statements of the executor. + * + * @return string + */ + public function serialize() + { + return serialize($this->_sqlStatements); + } + + + /** + * Reconstructs the executor with it's sql statements. + */ + public function unserialize($serialized) + { + $this->_sqlStatements = unserialize($serialized); + } +} \ No newline at end of file diff --git a/lib/Doctrine/Query/SqlExecutor/MultiTableDelete.php b/lib/Doctrine/Query/SqlExecutor/MultiTableDelete.php new file mode 100644 index 000000000..fadd2f23f --- /dev/null +++ b/lib/Doctrine/Query/SqlExecutor/MultiTableDelete.php @@ -0,0 +1,40 @@ +. + */ + +/** + * Executes the SQL statements for bulk DQL DELETE statements on classes in + * Class Table Inheritance (JOINED). + * + * @package Doctrine + * @subpackage Query + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 2.0 + * @version $Revision$ + * @todo For a good implementation that uses temporary tables see the Hibernate sources: + * (org.hibernate.hql.ast.exec.MultiTableDeleteExecutor). + */ +class Doctrine_Query_SqlExecutor_MultiTableDelete extends Doctrine_Query_SqlExecutor_Abstract +{ + + +} \ No newline at end of file diff --git a/lib/Doctrine/Query/SqlExecutor/MultiTableUpdate.php b/lib/Doctrine/Query/SqlExecutor/MultiTableUpdate.php new file mode 100644 index 000000000..6ff05e05c --- /dev/null +++ b/lib/Doctrine/Query/SqlExecutor/MultiTableUpdate.php @@ -0,0 +1,44 @@ +. + */ + +/** + * Executes the SQL statements for bulk DQL UPDATE statements on classes in + * Class Table Inheritance (JOINED). + * + * @package Doctrine + * @subpackage Query + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 2.0 + * @version $Revision$ + * @todo For a good implementation that uses temporary tables see the Hibernate sources: + * (org.hibernate.hql.ast.exec.MultiTableUpdateExecutor). + */ +class Doctrine_Query_SqlExecutor_MultiTableUpdate extends Doctrine_Query_SqlExecutor_Abstract +{ + public function __construct(Doctrine_Query_Production $AST) + { + // TODO: Inspect the AST, create the necessary SQL queries and store them + // in $this->_sqlStatements + } + +} \ No newline at end of file diff --git a/lib/Doctrine/Query/SqlExecutor/SingleSelect.php b/lib/Doctrine/Query/SqlExecutor/SingleSelect.php new file mode 100644 index 000000000..eb3f7cfe6 --- /dev/null +++ b/lib/Doctrine/Query/SqlExecutor/SingleSelect.php @@ -0,0 +1,45 @@ +. + */ + +/** + * Executor that executes the SQL statement for simple DQL SELECT statements. + * + * @package Doctrine + * @subpackage Abstract + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Roman Borschel + * @version $Revision$ + * @link www.phpdoctrine.org + * @since 2.0 + */ +class Doctrine_Query_SqlExecutor_SingleSelect extends Doctrine_Query_SqlExecutor_Abstract +{ + public function __construct(Doctrine_Query_Production $AST) + { + parent::__construct($AST); + $this->_sqlStatements = $AST->buildSql(); + } + + public function execute(Doctrine_Connection $conn, array $params) + { + return $conn->execute($this->_sqlStatements, $params); + } +} diff --git a/lib/Doctrine/Query/SqlExecutor/SingleTableDeleteUpdate.php b/lib/Doctrine/Query/SqlExecutor/SingleTableDeleteUpdate.php new file mode 100644 index 000000000..09e01b006 --- /dev/null +++ b/lib/Doctrine/Query/SqlExecutor/SingleTableDeleteUpdate.php @@ -0,0 +1,46 @@ +. + */ + +/** + * Executor that executes the SQL statements for DQL DELETE/UPDATE statements on classes + * that are mapped to a single table. + * + * @package Doctrine + * @subpackage SingleTableDeleteUpdate + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Roman Borschel + * @version $Revision$ + * @link www.phpdoctrine.org + * @since 2.0 + */ +class Doctrine_Query_SqlExecutor_SingleTableDeleteUpdate extends Doctrine_Query_SqlExecutor_Abstract +{ + public function __construct(Doctrine_Query_Production $AST) + { + parent::__construct($AST); + $this->_sqlStatements = $AST->buildSql(); + } + + public function execute(Doctrine_Connection $conn, array $params) + { + return $conn->exec($this->_sqlStatements, $params); + } +} \ No newline at end of file diff --git a/lib/Doctrine/Query/Token.php b/lib/Doctrine/Query/Token.php new file mode 100644 index 000000000..92b6cbe10 --- /dev/null +++ b/lib/Doctrine/Query/Token.php @@ -0,0 +1,151 @@ +. + */ + +/** + * Container for token type constants of Doctrine Query Language. + * + * @package Doctrine + * @subpackage Query + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +final class Doctrine_Query_Token +{ + const T_NONE = 1; + const T_IDENTIFIER = 2; + const T_INTEGER = 3; + const T_STRING = 4; + const T_INPUT_PARAMETER = 5; + const T_FLOAT = 6; + + const T_ALL = 101; + const T_AND = 102; + const T_ANY = 103; + const T_AS = 104; + const T_ASC = 105; + const T_AVG = 106; + const T_BETWEEN = 107; + const T_BY = 108; + const T_COUNT = 109; + const T_DELETE = 110; + const T_DESC = 111; + const T_DISTINCT = 112; + const T_ESCAPE = 113; + const T_EXISTS = 114; + const T_FROM = 115; + const T_GROUP = 116; + const T_HAVING = 117; + const T_IN = 118; + const T_INDEX = 119; + const T_INNER = 120; + const T_IS = 121; + const T_JOIN = 122; + const T_LEFT = 123; + const T_LIKE = 124; + const T_LIMIT = 125; + const T_MAX = 126; + const T_MIN = 127; + const T_MOD = 128; + const T_NOT = 129; + const T_NULL = 130; + const T_OFFSET = 131; + const T_ON = 132; + const T_OR = 133; + const T_ORDER = 134; + const T_SELECT = 135; + const T_SET = 136; + const T_SIZE = 137; + const T_SOME = 138; + const T_SUM = 139; + const T_UPDATE = 140; + const T_WHERE = 141; + const T_WITH = 142; + + const T_TRUE = 143; + const T_FALSE = 144; + + + protected $_keywordsTable = array(); + + + public function __construct() + { + $this->addKeyword(self::T_ALL, "ALL"); + $this->addKeyword(self::T_AND, "AND"); + $this->addKeyword(self::T_ANY, "ANY"); + $this->addKeyword(self::T_AS, "AS"); + $this->addKeyword(self::T_ASC, "ASC"); + $this->addKeyword(self::T_AVG, "AVG"); + $this->addKeyword(self::T_BETWEEN, "BETWEEN"); + $this->addKeyword(self::T_BY, "BY"); + $this->addKeyword(self::T_COUNT, "COUNT"); + $this->addKeyword(self::T_DELETE, "DELETE"); + $this->addKeyword(self::T_DESC, "DESC"); + $this->addKeyword(self::T_DISTINCT, "DISTINCT"); + $this->addKeyword(self::T_ESCAPE, "ESPACE"); + $this->addKeyword(self::T_EXISTS, "EXISTS"); + $this->addKeyword(self::T_FALSE, "FALSE"); + $this->addKeyword(self::T_FROM, "FROM"); + $this->addKeyword(self::T_GROUP, "GROUP"); + $this->addKeyword(self::T_HAVING, "HAVING"); + $this->addKeyword(self::T_IN, "IN"); + $this->addKeyword(self::T_INDEX, "INDEX"); + $this->addKeyword(self::T_INNER, "INNER"); + $this->addKeyword(self::T_IS, "IS"); + $this->addKeyword(self::T_JOIN, "JOIN"); + $this->addKeyword(self::T_LEFT, "LEFT"); + $this->addKeyword(self::T_LIKE, "LIKE"); + $this->addKeyword(self::T_LIMIT, "LIMIT"); + $this->addKeyword(self::T_MAX, "MAX"); + $this->addKeyword(self::T_MIN, "MIN"); + $this->addKeyword(self::T_MOD, "MOD"); + $this->addKeyword(self::T_NOT, "NOT"); + $this->addKeyword(self::T_NULL, "NULL"); + $this->addKeyword(self::T_OFFSET, "OFFSET"); + $this->addKeyword(self::T_ON, "ON"); + $this->addKeyword(self::T_OR, "OR"); + $this->addKeyword(self::T_ORDER, "ORDER"); + $this->addKeyword(self::T_SELECT, "SELECT"); + $this->addKeyword(self::T_SET, "SET"); + $this->addKeyword(self::T_SIZE, "SIZE"); + $this->addKeyword(self::T_SOME, "SOME"); + $this->addKeyword(self::T_SUM, "SUM"); + $this->addKeyword(self::T_TRUE, "TRUE"); + $this->addKeyword(self::T_UPDATE, "UPDATE"); + $this->addKeyword(self::T_WHERE, "WHERE"); + $this->addKeyword(self::T_WITH, "WITH"); + } + + + protected function addKeyword($token, $value) + { + $this->_keywordsTable[$token] = $value; + } + + + public function getLiteral($token) + { + return isset($this->_keywordsTable[$token]) ? $this->_keywordsTable[$token] : ''; + } +} diff --git a/lib/Doctrine/Query/Where.php b/lib/Doctrine/Query/Where.php deleted file mode 100644 index f25149109..000000000 --- a/lib/Doctrine/Query/Where.php +++ /dev/null @@ -1,180 +0,0 @@ -. - */ -Doctrine::autoload('Doctrine_Query_Condition'); -/** - * Doctrine_Query_Where - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - * @author Konsta Vesterinen - */ -class Doctrine_Query_Where extends Doctrine_Query_Condition -{ - public function load($where) - { - $where = $this->_tokenizer->bracketTrim(trim($where)); - $conn = $this->query->getConnection(); - $terms = $this->_tokenizer->sqlExplode($where); - - if (count($terms) > 1) { - if (substr($where, 0, 6) == 'EXISTS') { - return $this->parseExists($where, true); - } elseif (substr($where, 0, 10) == 'NOT EXISTS') { - return $this->parseExists($where, false); - } - } - - if (count($terms) < 3) { - $terms = $this->_tokenizer->sqlExplode($where, array('=', '<', '<>', '>', '!=')); - } - - if (count($terms) > 1) { - $first = array_shift($terms); - $value = array_pop($terms); - $operator = trim(substr($where, strlen($first), -strlen($value))); - $table = null; - $field = null; - - if (strpos($first, "'") === false && strpos($first, '(') === false) { - // normal field reference found - $a = explode('.', $first); - - $field = array_pop($a); - $reference = implode('.', $a); - - if (empty($reference)) { - $map = $this->query->getRootDeclaration(); - - $alias = $this->query->getTableAlias($this->query->getRootAlias()); - $table = $map['table']; - } else { - $map = $this->query->load($reference, false); - - $alias = $this->query->getTableAlias($reference); - $table = $map['table']; - } - } - $first = $this->query->parseClause($first); - - $sql = $first . ' ' . $operator . ' ' . $this->parseValue($value, $table, $field); - - return $sql; - } else { - - } - } - - public function parseValue($value, $table = null, $field = null) - { - $conn = $this->query->getConnection(); - - if (substr($value, 0, 1) == '(') { - // trim brackets - $trimmed = $this->_tokenizer->bracketTrim($value); - - if (substr($trimmed, 0, 4) == 'FROM' || - substr($trimmed, 0, 6) == 'SELECT') { - - // subquery found - $q = new Doctrine_Query(); - $value = '(' . $this->query->createSubquery()->parseQuery($trimmed, false)->getQuery() . ')'; - - } elseif (substr($trimmed, 0, 4) == 'SQL:') { - $value = '(' . substr($trimmed, 4) . ')'; - } else { - // simple IN expression found - $e = $this->_tokenizer->sqlExplode($trimmed, ','); - - $value = array(); - - $index = false; - - foreach ($e as $part) { - if (isset($table) && isset($field)) { - $index = $table->enumIndex($field, trim($part, "'")); - - if (false !== $index && $conn->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)) { - $index = $conn->quote($index, 'text'); - } - } - - if ($index !== false) { - $value[] = $index; - } else { - $value[] = $this->parseLiteralValue($part); - } - } - - $value = '(' . implode(', ', $value) . ')'; - } - } else if (substr($value, 0, 1) == ':' || $value === '?') { - // placeholder found - if (isset($table) && isset($field) && $table->getTypeOf($field) == 'enum') { - $this->query->addEnumParam($value, $table, $field); - } else { - $this->query->addEnumParam($value, null, null); - } - } else { - $enumIndex = false; - if (isset($table) && isset($field)) { - // check if value is enumerated value - $enumIndex = $table->enumIndex($field, trim($value, "'")); - - if (false !== $enumIndex && $conn->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)) { - $enumIndex = $conn->quote($enumIndex, 'text'); - } - } - - if ($enumIndex !== false) { - $value = $enumIndex; - } else { - $value = $this->parseLiteralValue($value); - } - } - return $value; - } - - /** - * parses an EXISTS expression - * - * @param string $where query where part to be parsed - * @param boolean $negation whether or not to use the NOT keyword - * @return string - */ - public function parseExists($where, $negation) - { - $operator = ($negation) ? 'EXISTS' : 'NOT EXISTS'; - - $pos = strpos($where, '('); - - if ($pos == false) { - throw new Doctrine_Query_Exception('Unknown expression, expected a subquery with () -marks'); - } - - $sub = $this->_tokenizer->bracketTrim(substr($where, $pos)); - - return $operator . ' (' . $this->query->createSubquery()->parseQuery($sub, false)->getQuery() . ')'; - } -} diff --git a/lib/Doctrine/Query_old.php b/lib/Doctrine/Query_old.php new file mode 100644 index 000000000..780736104 --- /dev/null +++ b/lib/Doctrine/Query_old.php @@ -0,0 +1,590 @@ +. + */ +Doctrine::autoload('Doctrine_Query_Abstract'); +/** + * Doctrine_Query + * A Doctrine_Query object represents a DQL query. It is used to query databases for + * data in an object-oriented fashion. A DQL query understands relations and inheritance + * and is dbms independant. + * + * @package Doctrine + * @subpackage Query + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + * @author Konsta Vesterinen + * @todo Proposal: This class does far too much. It should have only 1 task: Collecting + * the DQL query parts and the query parameters (the query state and caching options/methods + * can remain here, too). + * The actual SQL construction could be done by a separate object (Doctrine_Query_SqlBuilder?) + * whose task it is to convert DQL into SQL. + * Furthermore the SqlBuilder? can then use other objects (Doctrine_Query_Tokenizer?), + * (Doctrine_Query_Parser(s)?) to accomplish his work. Doctrine_Query does not need + * to know the tokenizer/parsers. There could be extending + * implementations of SqlBuilder? that cover the specific SQL dialects. + * This would release Doctrine_Connection and the Doctrine_Connection_xxx classes + * from this tedious task. + * This would also largely reduce the currently huge interface of Doctrine_Query(_Abstract) + * and better hide all these transformation internals from the public Query API. + * + * @internal The lifecycle of a Query object is the following: + * After construction the query object is empty. Through using the fluent + * query interface the user fills the query object with DQL parts and query parameters. + * These get collected in {@link $_dqlParts} and {@link $_params}, respectively. + * When the query is executed the first time, or when {@link getSqlQuery()} + * is called the first time, the collected DQL parts get parsed and the resulting + * connection-driver specific SQL is generated. The generated SQL parts are + * stored in {@link $_sqlParts} and the final resulting SQL query is stored in + * {@link $_sql}. + */ +class Doctrine_Query extends Doctrine_Query_Abstract implements Countable, Serializable +{ + /** + * @var array + */ + protected $_subqueryAliases = array(); + + /** + * @var array $_aggregateAliasMap an array containing all aggregate aliases, keys as dql aliases + * and values as sql aliases + */ + protected $_aggregateAliasMap = array(); + + /** + * @param boolean $needsSubquery + */ + protected $_needsSubquery = false; + + /** + * @param boolean $isSubquery whether or not this query object is a subquery of another + * query object + */ + protected $_isSubquery; + + /** + * @var array $_neededTables an array containing the needed table aliases + */ + protected $_neededTables = array(); + + /** + * @var array + */ + protected $_expressionMap = array(); + + /** + * @var string $_sql cached SQL query + */ + protected $_sql; + + protected $_dql; + + + /** + * create + * returns a new Doctrine_Query object + * + * @param Doctrine_Connection $conn optional connection parameter + * @return Doctrine_Query + */ + public static function create($conn = null) + { + return new Doctrine_Query($conn); + } + + /** + * Resets the query to the state just after it has been instantiated. + */ + public function reset() + { + $this->_neededTables = array(); + $this->_expressionMap = array(); + $this->_subqueryAliases = array(); + $this->_needsSubquery = false; + $this->_isLimitSubqueryUsed = false; + } + + /** + * createSubquery + * creates a subquery + * + * @return Doctrine_Hydrate + */ + public function createSubquery() + { + $class = get_class($this); + $obj = new $class(); + + // copy the aliases to the subquery + $obj->copyAliases($this); + + // this prevents the 'id' being selected, re ticket #307 + $obj->isSubquery(true); + + return $obj; + } + + /** + * addEnumParam + * sets input parameter as an enumerated parameter + * + * @param string $key the key of the input parameter + * @return Doctrine_Query + */ + public function addEnumParam($key, $table = null, $column = null) + { + $array = (isset($table) || isset($column)) ? array($table, $column) : array(); + + if ($key === '?') { + $this->_enumParams[] = $array; + } else { + $this->_enumParams[$key] = $array; + } + } + + /** + * getEnumParams + * get all enumerated parameters + * + * @return array all enumerated parameters + */ + public function getEnumParams() + { + return $this->_enumParams; + } + + /** + * getDql + * returns the DQL query that is represented by this query object. + * + * the query is built from $_dqlParts + * + * @return string the DQL query + */ + public function getDql() + { + if ($this->_dql !== null) { + return $this->_dql; + } + + $q = ''; + $q .= ( ! empty($this->_dqlParts['select']))? 'SELECT ' . implode(', ', $this->_dqlParts['select']) : ''; + $q .= ( ! empty($this->_dqlParts['from']))? ' FROM ' . implode(' ', $this->_dqlParts['from']) : ''; + $q .= ( ! empty($this->_dqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_dqlParts['where']) : ''; + $q .= ( ! empty($this->_dqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_dqlParts['groupby']) : ''; + $q .= ( ! empty($this->_dqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_dqlParts['having']) : ''; + $q .= ( ! empty($this->_dqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_dqlParts['orderby']) : ''; + $q .= ( ! empty($this->_dqlParts['limit']))? ' LIMIT ' . implode(' ', $this->_dqlParts['limit']) : ''; + $q .= ( ! empty($this->_dqlParts['offset']))? ' OFFSET ' . implode(' ', $this->_dqlParts['offset']) : ''; + + return $q; + } + + /** + * getParams + * + * @return array + */ + public function getParams() + { + return array_merge($this->_params['join'], $this->_params['set'], $this->_params['where'], $this->_params['having']); + } + + /** + * setParams + * + * @param array $params + */ + public function setParams(array $params = array()) { + $this->_params = $params; + } + + /** + * fetchArray + * Convenience method to execute using array fetching as hydration mode. + * + * @param string $params + * @return array + */ + public function fetchArray($params = array()) { + return $this->execute($params, Doctrine::HYDRATE_ARRAY); + } + + /** + * fetchOne + * Convenience method to execute the query and return the first item + * of the collection. + * + * @param string $params Parameters + * @param int $hydrationMode Hydration mode + * @return mixed Array or Doctrine_Collection or false if no result. + */ + public function fetchOne($params = array(), $hydrationMode = null) + { + $collection = $this->execute($params, $hydrationMode); + + if (count($collection) === 0) { + return false; + } + + if ($collection instanceof Doctrine_Collection) { + return $collection->getFirst(); + } else if (is_array($collection)) { + return array_shift($collection); + } + + return false; + } + + /** + * isSubquery + * if $bool parameter is set this method sets the value of + * Doctrine_Query::$isSubquery. If this value is set to true + * the query object will not load the primary key fields of the selected + * components. + * + * If null is given as the first parameter this method retrieves the current + * value of Doctrine_Query::$isSubquery. + * + * @param boolean $bool whether or not this query acts as a subquery + * @return Doctrine_Query|bool + */ + public function isSubquery($bool = null) + { + if ($bool === null) { + return $this->_isSubquery; + } + + $this->_isSubquery = (bool) $bool; + return $this; + } + + /** + * getAggregateAlias + * + * @param string $dqlAlias the dql alias of an aggregate value + * @return string + * @deprecated + */ + public function getAggregateAlias($dqlAlias) + { + return $this->getSqlAggregateAlias($dqlAlias); + } + + /** + * getSqlAggregateAlias + * + * @param string $dqlAlias the dql alias of an aggregate value + * @return string + */ + public function getSqlAggregateAlias($dqlAlias) + { + if (isset($this->_aggregateAliasMap[$dqlAlias])) { + // mark the expression as used + $this->_expressionMap[$dqlAlias][1] = true; + + return $this->_aggregateAliasMap[$dqlAlias]; + } else if ( ! empty($this->_pendingAggregates)) { + $this->processPendingAggregates(); + + return $this->getSqlAggregateAlias($dqlAlias); + } else { + throw new Doctrine_Query_Exception('Unknown aggregate alias: ' . $dqlAlias); + } + } + + /** + * getDqlPart + * returns a specific DQL query part. + * + * @param string $queryPart the name of the query part + * @return string the DQL query part + * @todo Description: List which query parts exist or point to the method/property + * where they are listed. + */ + public function getDqlPart($queryPart) + { + if ( ! isset($this->_dqlParts[$queryPart])) { + throw new Doctrine_Query_Exception('Unknown query part ' . $queryPart); + } + + return $this->_dqlParts[$queryPart]; + } + + /** + * contains + * + * Method to check if a arbitrary piece of dql exists + * + * @param string $dql Arbitrary piece of dql to check for + * @return boolean + */ + public function contains($dql) + { + return stripos($this->getDql(), $dql) === false ? false : true; + } + + + public function parseSubquery($subquery) + { + $trimmed = trim($this->_tokenizer->bracketTrim($subquery)); + + // check for possible subqueries + if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { + // parse subquery + $trimmed = $this->createSubquery()->parseDqlQuery($trimmed)->getQuery(); + } else { + // parse normal clause + $trimmed = $this->parseClause($trimmed); + } + + return '(' . $trimmed . ')'; + } + + /** + * preQuery + * + * Empty template method to provide Query subclasses with the possibility + * to hook into the query building procedure, doing any custom / specialized + * query building procedures that are neccessary. + * + * @return void + */ + public function preQuery() + { + + } + + /** + * postQuery + * + * Empty template method to provide Query subclasses with the possibility + * to hook into the query building procedure, doing any custom / specialized + * post query procedures (for example logging) that are neccessary. + * + * @return void + */ + public function postQuery() + { + + } + + /** + * builds the sql query from the given parameters and applies things such as + * column aggregation inheritance and limit subqueries if needed + * + * @param array $params an array of prepared statement params (needed only in mysql driver + * when limit subquery algorithm is used) + * @return string the built sql query + */ + public function getSqlQuery($params = array()) + { + if ($this->_state === self::STATE_DIRTY) { + $this->parse(); + } + + return $this->_sql; + } + + public function parse() + { + $this->reset(); + + // invoke the preQuery hook + $this->preQuery(); + + $parser = new Doctrine_Query_Parser($this); + $parser->parse(); + + if ($parser->isErrors()) { + throw new Doctrine_Query_Parser_Exception( + "Errors were detected during query parsing:\n" . + implode("\n", $parser->getErrors()) + ); + } + + $this->_state = self::STATE_CLEAN; + $this->_sql = $parser->getSql(); + } + + /** + * count + * fetches the count of the query + * + * This method executes the main query without all the + * selected fields, ORDER BY part, LIMIT part and OFFSET part. + * + * Example: + * Main query: + * SELECT u.*, p.phonenumber FROM User u + * LEFT JOIN u.Phonenumber p + * WHERE p.phonenumber = '123 123' LIMIT 10 + * + * The modified DQL query: + * SELECT COUNT(DISTINCT u.id) FROM User u + * LEFT JOIN u.Phonenumber p + * WHERE p.phonenumber = '123 123' + * + * @param array $params an array of prepared statement parameters + * @return integer the count of this query + */ + public function count($params = array()) + { + // triggers dql parsing/processing + $this->getQuery(); // this is ugly + + // initialize temporary variables + $where = $this->_sqlParts['where']; + $having = $this->_sqlParts['having']; + $groupby = $this->_sqlParts['groupby']; + $map = reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + $table = $map['table']; + + // build the query base + $q = 'SELECT COUNT(DISTINCT ' . $this->getTableAlias($componentAlias) + . '.' . implode(',', $table->getIdentifierColumnNames()) + . ') AS num_results'; + + foreach ($this->_sqlParts['select'] as $field) { + if (strpos($field, '(') !== false) { + $q .= ', ' . $field; + } + } + + $q .= ' FROM ' . $this->_buildSqlFromPart(); + + // append discriminator column conditions (if any) + $string = $this->_createDiscriminatorConditionSql(); + if ( ! empty($string)) { + $where[] = $string; + } + + // append conditions + $q .= ( ! empty($where)) ? ' WHERE ' . implode(' AND ', $where) : ''; + $q .= ( ! empty($groupby)) ? ' GROUP BY ' . implode(', ', $groupby) : ''; + $q .= ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having): ''; + + if ( ! is_array($params)) { + $params = array($params); + } + // append parameters + $params = array_merge($this->_params['where'], $this->_params['having'], $params); + + $params = $this->convertEnums($params); + + $results = $this->getConnection()->fetchAll($q, $params); + + if (count($results) > 1) { + $count = 0; + foreach ($results as $result) { + $count += $result['num_results']; + } + } else { + $count = isset($results[0]) ? $results[0]['num_results']:0; + } + + return (int) $count; + } + + public function setDql($query) + { + $this->_dql = $query; + $this->_state = self::STATE_DIRTY; + } + + /** + * query + * query the database with DQL (Doctrine Query Language) + * + * @param string $query DQL query + * @param array $params prepared statement parameters + * @param int $hydrationMode Doctrine::FETCH_ARRAY or Doctrine::FETCH_RECORD + * @see Doctrine::FETCH_* constants + * @return mixed + */ + public function query($query, $params = array(), $hydrationMode = null) + { + $this->setDql($query); + return $this->execute($params, $hydrationMode); + } + + /** + * Copies a Doctrine_Query object. + * + * @param Doctrine_Query Doctrine query instance. + * If ommited the instance itself will be used as source. + * @return Doctrine_Query Copy of the Doctrine_Query instance. + */ + public function copy(Doctrine_Query $query = null) + { + if ( ! $query) { + $query = $this; + } + + $new = new Doctrine_Query(); + $new->_dqlParts = $query->_dqlParts; + $new->_params = $query->_params; + $new->_hydrator = $query->_hydrator; + + return $new; + } + + /** + * Frees the resources used by the query object. It especially breaks a + * cyclic reference between the query object and it's parsers. This enables + * PHP's current GC to reclaim the memory. + * This method can therefore be used to reduce memory usage when creating a lot + * of query objects during a request. + * + * @return Doctrine_Query this object + */ + public function free() + { + $this->reset(); + $this->_parsers = array(); + $this->_dqlParts = array(); + $this->_enumParams = array(); + } + + /** + * serialize + * this method is automatically called when this Doctrine_Hydrate is serialized + * + * @return array an array of serialized properties + */ + public function serialize() + { + $vars = get_object_vars($this); + } + + /** + * unseralize + * this method is automatically called everytime a Doctrine_Hydrate object is unserialized + * + * @param string $serialized Doctrine_Record as serialized string + * @return void + */ + public function unserialize($serialized) + { + + } +} diff --git a/lib/Doctrine/Relation.php b/lib/Doctrine/Relation.php index 91abfdb5b..eb527d382 100644 --- a/lib/Doctrine/Relation.php +++ b/lib/Doctrine/Relation.php @@ -236,6 +236,18 @@ abstract class Doctrine_Relation implements ArrayAccess { return $this->definition['relName']; } + + /** + * getTable + * returns the foreign table object + * + * @return object Doctrine_Table + */ + final public function getTable() + { + return Doctrine_EntityManager::getManager($this->definition['class']) + ->getClassMetadata($this->definition['class']); + } /** * getType @@ -255,7 +267,7 @@ abstract class Doctrine_Relation implements ArrayAccess * * @return object Doctrine_Table */ - final public function getTable() + final public function getClassMetadata() { return Doctrine_EntityManager::getManager($this->definition['class']) ->getClassMetadata($this->definition['class']); @@ -334,7 +346,7 @@ abstract class Doctrine_Relation implements ArrayAccess */ public function getRelationDql($count) { - $component = $this->getTable()->getComponentName(); + $component = $this->getClassMetadata()->getComponentName(); $dql = 'FROM ' . $component . ' WHERE ' . $component . '.' . $this->definition['foreign'] diff --git a/tests/Orm/AllTests.php b/tests/Orm/AllTests.php index 467ee6e97..472cfba97 100644 --- a/tests/Orm/AllTests.php +++ b/tests/Orm/AllTests.php @@ -7,6 +7,7 @@ require_once 'lib/DoctrineTestInit.php'; // Suites require_once 'Orm/Component/AllTests.php'; +require_once 'Orm/Query/AllTests.php'; require_once 'Orm/Hydration/AllTests.php'; require_once 'Orm/Ticket/AllTests.php'; @@ -28,13 +29,14 @@ class Orm_AllTests //$suite->addTestSuite('Orm_ConfigurableTestCase'); $suite->addTest(Orm_Component_AllTests::suite()); + $suite->addTest(Orm_Query_AllTests::suite()); $suite->addTest(Orm_Hydration_AllTests::suite()); $suite->addTest(Orm_Ticket_AllTests::suite()); - + return $suite; } } if (PHPUnit_MAIN_METHOD == 'Orm_AllTests::main') { Orm_AllTests::main(); -} \ No newline at end of file +} diff --git a/tests/Orm/Component/AllTests.php b/tests/Orm/Component/AllTests.php index 1e4d102ff..e29806a21 100644 --- a/tests/Orm/Component/AllTests.php +++ b/tests/Orm/Component/AllTests.php @@ -28,5 +28,5 @@ class Orm_Component_AllTests } if (PHPUnit_MAIN_METHOD == 'Orm_Component_AllTests::main') { - Dbal_Component_AllTests::main(); + Orm_Component_AllTests::main(); } diff --git a/tests/Orm/Query/AllTests.php b/tests/Orm/Query/AllTests.php new file mode 100755 index 000000000..51ede62c2 --- /dev/null +++ b/tests/Orm/Query/AllTests.php @@ -0,0 +1,41 @@ +addTestSuite('Orm_Query_IdentifierRecognitionTest'); + $suite->addTestSuite('Orm_Query_LanguageRecognitionTest'); + $suite->addTestSuite('Orm_Query_ScannerTest'); + $suite->addTestSuite('Orm_Query_DqlGenerationTest'); + $suite->addTestSuite('Orm_Query_DeleteSqlGenerationTest'); + $suite->addTestSuite('Orm_Query_UpdateSqlGenerationTest'); + $suite->addTestSuite('Orm_Query_SelectSqlGenerationTest'); + + return $suite; + } +} + +if (PHPUnit_MAIN_METHOD == 'Orm_Query_AllTests::main') { + Orm_Query_AllTests::main(); +} diff --git a/tests/Orm/Query/DeleteSqlGenerationTest.php b/tests/Orm/Query/DeleteSqlGenerationTest.php new file mode 100755 index 000000000..8533ad9a5 --- /dev/null +++ b/tests/Orm/Query/DeleteSqlGenerationTest.php @@ -0,0 +1,270 @@ +. + */ +require_once 'lib/DoctrineTestInit.php'; +/** + * Test case for testing the saving and referencing of query identifiers. + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @author Konsta Vesterinen + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + * @todo 1) [romanb] We might want to split the SQL generation tests into multiple + * testcases later since we'll have a lot of them and we might want to have special SQL + * generation tests for some dbms specific SQL syntaxes. + */ +class Orm_Query_DeleteSqlGenerationTest extends Doctrine_OrmTestCase +{ + public function testWithoutWhere() + { + $q = new Doctrine_Query(); + + // NO WhereClause + $q->setDql('DELETE CmsUser u'); + $this->assertEquals('DELETE FROM cms_user cu WHERE 1 = 1', $q->getSql()); + $q->free(); + + $q->setDql('DELETE FROM CmsUser u'); + $this->assertEquals('DELETE FROM cms_user cu WHERE 1 = 1', $q->getSql()); + $q->free(); + } + + + public function testWithWhere() + { + $q = new Doctrine_Query(); + + // "WHERE" ConditionalExpression + // ConditionalExpression = ConditionalTerm {"OR" ConditionalTerm} + // ConditionalTerm = ConditionalFactor {"AND" ConditionalFactor} + // ConditionalFactor = ["NOT"] ConditionalPrimary + // ConditionalPrimary = SimpleConditionalExpression | "(" ConditionalExpression ")" + // SimpleConditionalExpression + // = Expression (ComparisonExpression | BetweenExpression | LikeExpression + // | InExpression | NullComparisonExpression) | ExistsExpression + + // If this one test fail, all others will fail too. That's the simplest case possible + $q->setDql('DELETE CmsUser u WHERE id = ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id = ?', $q->getSql()); + $q->free(); + } + + + public function testWithConditionalExpressions() + { + $q = new Doctrine_Query(); + + $q->setDql('DELETE CmsUser u WHERE u.username = ? OR u.name = ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.username = ? OR cu.name = ?', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE u.id = ? OR ( u.username = ? OR u.name = ? )'); + $this->assertEquals( + 'DELETE FROM cms_user cu WHERE cu.id = ? OR (cu.username = ? OR cu.name = ?)', + $q->getSql() + ); + $q->free(); + + $q->setDql('DELETE FROM CmsUser WHERE id = ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id = ?', $q->getSql()); + $q->free(); + } + + + public function testInvalidSyntaxIsRejected() + { + $q = new Doctrine_Query(); + + $invalidDql = 'FOOBAR CmsUser'; + $q->setDql($invalidDql); + try { + $q->getSql(); + $this->fail("Invalid DQL '$invalidDql' was not rejected."); + } catch (Doctrine_Query_Parser_Exception $parseEx) {} + + $invalidDql = 'DELETE FROM hey.boy'; + $q->setDql($invalidDql); + try { + $q->getSql(); + $this->fail("Invalid DQL '$invalidDql' was not rejected."); + } catch (Doctrine_Query_Parser_Exception $parseEx) {} + + $invalidDql = 'DELETE FROM CmsUser cu WHERE cu.my.thing > ?'; + $q->setDql($invalidDql); + try { + $q->getSql(); + $this->fail("Invalid DQL '$invalidDql' was not rejected."); + } catch (Doctrine_Relation_Exception $parseEx) {} + + } + + + public function testParserIsCaseAgnostic() + { + $q = new Doctrine_Query(); + $q->setDql('delete from CmsUser u where u.username = ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.username = ?', $q->getSql()); + } + + + public function testWithConditionalTerms() + { + $q = new Doctrine_Query(); + + $q->setDql('DELETE CmsUser u WHERE u.username = ? AND u.name = ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.username = ? AND cu.name = ?', $q->getSql()); + $q->free(); + } + + + public function testWithConditionalFactors() + { + $q = new Doctrine_Query(); + + $q->setDql('DELETE CmsUser u WHERE NOT id != ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE NOT cu.id <> ?', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE NOT ( id != ? )'); + $this->assertEquals('DELETE FROM cms_user cu WHERE NOT (cu.id <> ?)', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE NOT ( id != ? AND username = ? )'); + $this->assertEquals('DELETE FROM cms_user cu WHERE NOT (cu.id <> ? AND cu.username = ?)', $q->getSql()); + $q->free(); + } + + + // ConditionalPrimary was already tested (see testDeleteWithWhere() and testDeleteWithConditionalFactors()) + + + public function testWithExprAndComparison() + { + $q = new Doctrine_Query(); + + // id = ? was already tested (see testDeleteWithWhere()) + + $q->setDql('DELETE CmsUser u WHERE id > ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id > ?', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE id >= ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id >= ?', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE id < ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id < ?', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE id <= ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id <= ?', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE id <> ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id <> ?', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE id != ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id <> ?', $q->getSql()); + $q->free(); + } + + + public function testWithExprAndBetween() + { + $q = new Doctrine_Query(); + + // "WHERE" Expression BetweenExpression + $q->setDql('DELETE CmsUser u WHERE u.id NOT BETWEEN ? AND ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id NOT BETWEEN ? AND ?', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE u.id BETWEEN ? AND ? AND u.username != ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id BETWEEN ? AND ? AND cu.username <> ?', $q->getSql()); + $q->free(); + } + + + public function testWithExprAndLike() + { + $q = new Doctrine_Query(); + + // "WHERE" Expression LikeExpression + $q->setDql('DELETE CmsUser u WHERE u.username NOT LIKE ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.username NOT LIKE ?', $q->getSql()); + $q->free(); + + $q->setDql("DELETE CmsUser u WHERE u.username LIKE ? ESCAPE '\\'"); + $this->assertEquals("DELETE FROM cms_user cu WHERE cu.username LIKE ? ESCAPE '\\'", $q->getSql()); + $q->free(); + } + + + public function testWithExprAndIn() + { + $q = new Doctrine_Query(); + + // "WHERE" Expression InExpression + $q->setDql('DELETE CmsUser u WHERE u.id IN ( ?, ?, ?, ? )'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id IN (?, ?, ?, ?)', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE u.id NOT IN ( ?, ? )'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id NOT IN (?, ?)', $q->getSql()); + $q->free(); + } + + + public function testWithExprAndNull() + { + $q = new Doctrine_Query(); + + // "WHERE" Expression NullComparisonExpression + $q->setDql('DELETE CmsUser u WHERE u.name IS NULL'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.name IS NULL', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE u.name IS NOT NULL'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.name IS NOT NULL', $q->getSql()); + $q->free(); + } + + + // All previously defined tests used Primary as PathExpression. No need to check it again. + + public function testWithPrimaryAsAtom() + { + $q = new Doctrine_Query(); + + // Atom = string | integer | float | boolean | input_parameter + $q->setDql('DELETE CmsUser u WHERE 1 = 1'); + $this->assertEquals('DELETE FROM cms_user cu WHERE 1 = 1', $q->getSql()); + $q->free(); + + $q->setDql('DELETE CmsUser u WHERE ? = 1'); + $this->assertEquals('DELETE FROM cms_user cu WHERE ? = 1', $q->getSql()); + $q->free(); + } +} diff --git a/tests/Orm/Query/DqlGenerationTest.php b/tests/Orm/Query/DqlGenerationTest.php new file mode 100755 index 000000000..2393a2bf2 --- /dev/null +++ b/tests/Orm/Query/DqlGenerationTest.php @@ -0,0 +1,249 @@ +. + */ + +/** + * Test case for testing the saving and referencing of query identifiers. + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @author Konsta Vesterinen + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Orm_Query_DqlGenerationTest extends Doctrine_OrmTestCase +{ + const QueryClass = 'Doctrine_Query'; + + public function testSelect() + { + $class = self::QueryClass; + $q = new $class(); + + // select and from + $q->setDql('FROM User u'); + $this->assertEquals('FROM User u', $q->getDql()); // Internally we use SELECT * FROM User u to process the DQL + $q->free(); + + $q->select()->from('User u'); + $this->assertEquals('SELECT * FROM User u', $q->getDql()); + $q->free(); + + $q->select('u.*')->from('User u'); + $this->assertEquals('SELECT u.* FROM User u', $q->getDql()); + $q->free(); + + $q->select('u.id')->from('User u'); + $this->assertEquals('SELECT u.id FROM User u', $q->getDql()); + $q->free(); + + $q->select('u.id, u.name')->from('User u'); + $this->assertEquals('SELECT u.id, u.name FROM User u', $q->getDql()); + $q->free(); + + $q->select('u.name AS myCustomName')->from('User u'); + $this->assertEquals('SELECT u.name AS myCustomName FROM User u', $q->getDql()); + $q->free(); + + $q->select('u.id')->select('u.name')->from('User u'); + $this->assertEquals('SELECT u.id, u.name FROM User u', $q->getDql()); + $q->free(); + } + + + public function testSelectDistinct() + { + $class = self::QueryClass; + $q = new $class(); + + $q->select()->distinct()->from('User u'); + $this->assertEquals('SELECT DISTINCT * FROM User u', $q->getDql()); + $q->free(); + + $q->select('u.name')->distinct(false)->from('User u'); + $this->assertEquals('SELECT u.name FROM User u', $q->getDql()); + $q->free(); + + $q->select()->distinct(false)->from('User u'); + $this->assertEquals('SELECT * FROM User u', $q->getDql()); + $q->free(); + + $q->select('u.name')->distinct()->from('User u'); + $this->assertEquals('SELECT DISTINCT u.name FROM User u', $q->getDql()); + $q->free(); + + $q->select('u.name, u.email')->distinct()->from('User u'); + $this->assertEquals('SELECT DISTINCT u.name, u.email FROM User u', $q->getDql()); + $q->free(); + + $q->select('u.name')->select('u.email')->distinct()->from('User u'); + $this->assertEquals('SELECT DISTINCT u.name, u.email FROM User u', $q->getDql()); + $q->free(); + + $q->select('DISTINCT u.name')->from('User u'); + $this->assertEquals('SELECT DISTINCT u.name FROM User u', $q->getDql()); + $q->free(); + + $q->select('DISTINCT u.name, u.email')->from('User u'); + $this->assertEquals('SELECT DISTINCT u.name, u.email FROM User u', $q->getDql()); + $q->free(); + + $q->select('DISTINCT u.name')->select('u.email')->from('User u'); + $this->assertEquals('SELECT DISTINCT u.name, u.email FROM User u', $q->getDql()); + $q->free(); + } + + + public function testSelectJoin() + { + $class = self::QueryClass; + $q = new $class(); + + $q->select('u.*')->from('User u')->join('u.Group g')->where('g.id = ?', 1); + $this->assertEquals('SELECT u.* FROM User u INNER JOIN u.Group g WHERE g.id = ?', $q->getDql()); + $this->assertEquals(array(1), $q->getParams()); + $q->free(); + + $q->select('u.*')->from('User u')->innerJoin('u.Group g')->where('g.id = ?', 1); + $this->assertEquals('SELECT u.* FROM User u INNER JOIN u.Group g WHERE g.id = ?', $q->getDql()); + $this->assertEquals(array(1), $q->getParams()); + $q->free(); + + $q->select('u.*')->from('User u')->leftJoin('u.Group g')->where('g.id IS NULL'); + $this->assertEquals('SELECT u.* FROM User u LEFT JOIN u.Group g WHERE g.id IS NULL', $q->getDql()); + $q->free(); + + $q->select('u.*')->from('User u')->leftJoin('u.UserGroup ug')->leftJoin('ug.Group g')->where('g.name = ?', 'admin'); + $this->assertEquals('SELECT u.* FROM User u LEFT JOIN u.UserGroup ug LEFT JOIN ug.Group g WHERE g.name = ?', $q->getDql()); + $q->free(); + } + + + public function testSelectWhere() + { + $class = self::QueryClass; + $q = new $class(); + + $q->select('u.name')->from('User u')->where('u.id = ?', 1); + $this->assertEquals('SELECT u.name FROM User u WHERE u.id = ?', $q->getDql()); + $this->assertEquals(array(1), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->where('u.id = ? AND u.type != ?', array(1, 'admin')); + $this->assertEquals('SELECT u.name FROM User u WHERE u.id = ? AND u.type != ?', $q->getDql()); + $this->assertEquals(array(1, 'admin'), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->where('u.id = ?', 1)->andWhere('u.type != ?', 'admin'); + $this->assertEquals('SELECT u.name FROM User u WHERE u.id = ? AND u.type != ?', $q->getDql()); + $this->assertEquals(array(1, 'admin'), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->where('( u.id = ?', 1)->andWhere('u.type != ? )', 'admin'); + $this->assertEquals('SELECT u.name FROM User u WHERE ( u.id = ? AND u.type != ? )', $q->getDql()); + $this->assertEquals(array(1, 'admin'), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->where('u.id = ? OR u.type != ?', array(1, 'admin')); + $this->assertEquals('SELECT u.name FROM User u WHERE u.id = ? OR u.type != ?', $q->getDql()); + $this->assertEquals(array(1, 'admin'), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->where('u.id = ?', 1)->orWhere('u.type != ?', 'admin'); + $this->assertEquals('SELECT u.name FROM User u WHERE u.id = ? OR u.type != ?', $q->getDql()); + $this->assertEquals(array(1, 'admin'), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->andwhere('u.id = ?', 1)->andWhere('u.type != ?', 'admin')->orWhere('u.email = ?', 'admin@localhost'); + $this->assertEquals('SELECT u.name FROM User u WHERE u.id = ? AND u.type != ? OR u.email = ?', $q->getDql()); + $this->assertEquals(array(1, 'admin', 'admin@localhost'), $q->getParams()); + $q->free(); + } + + + public function testSelectWhereIn() + { + $class = self::QueryClass; + $q = new $class(); + + $q->select('u.name')->from('User u')->whereIn('u.id', array(1, 2, 3, 4, 5)); + $this->assertEquals('SELECT u.name FROM User u WHERE u.id IN (?, ?, ?, ?, ?)', $q->getDql()); + $this->assertEquals(array(1, 2, 3, 4, 5), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->whereNotIn('u.id', array(1, 2, 3)); + $this->assertEquals('SELECT u.name FROM User u WHERE u.id NOT IN (?, ?, ?)', $q->getDql()); + $this->assertEquals(array(1, 2, 3), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->where('u.type = ?', 'admin')->andWhereIn('u.id', array(1, 2)); + $this->assertEquals('SELECT u.name FROM User u WHERE u.type = ? AND u.id IN (?, ?)', $q->getDql()); + $this->assertEquals(array('admin', 1, 2), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->where('u.type = ?', 'admin')->andWhereNotIn('u.id', array(1, 2)); + $this->assertEquals('SELECT u.name FROM User u WHERE u.type = ? AND u.id NOT IN (?, ?)', $q->getDql()); + $this->assertEquals(array('admin', 1, 2), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->whereIn('u.type', array('admin', 'moderator'))->andWhereNotIn('u.id', array(1, 2, 3, 4)); + $this->assertEquals('SELECT u.name FROM User u WHERE u.type IN (?, ?) AND u.id NOT IN (?, ?, ?, ?)', $q->getDql()); + $this->assertEquals(array('admin', 'moderator', 1, 2, 3, 4), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->whereIn('u.type', array('admin', 'moderator'))->orWhereIn('u.id', array(1, 2, 3, 4)); + $this->assertEquals('SELECT u.name FROM User u WHERE u.type IN (?, ?) OR u.id IN (?, ?, ?, ?)', $q->getDql()); + $this->assertEquals(array('admin', 'moderator', 1, 2, 3, 4), $q->getParams()); + $q->free(); + + $q->select('u.name')->from('User u')->whereIn('u.type', array('admin', 'moderator'))->andWhereNotIn('u.id', array(1, 2))->orWhereNotIn('u.type', array('admin', 'moderator'))->andWhereNotIn('u.email', array('user@localhost', 'guest@localhost')); + $this->assertEquals('SELECT u.name FROM User u WHERE u.type IN (?, ?) AND u.id NOT IN (?, ?) OR u.type NOT IN (?, ?) AND u.email NOT IN (?, ?)', $q->getDql()); + $this->assertEquals(array('admin', 'moderator', 1, 2, 'admin', 'moderator', 'user@localhost', 'guest@localhost'), $q->getParams()); + $q->free(); + } + + + public function testDelete() + { + $class = self::QueryClass; + $q = new $class(); + + $q->setDql('DELETE CmsUser u'); + $this->assertEquals('DELETE CmsUser u', $q->getDql()); + $q->free(); + + $q->delete()->from('CmsUser u'); + $this->assertEquals('DELETE FROM CmsUser u', $q->getDql()); + $q->free(); + + $q->delete()->from('CmsUser u')->where('u.id = ?', 1); + $this->assertEquals('DELETE FROM CmsUser u WHERE u.id = ?', $q->getDql()); + $q->free(); + + $q->delete()->from('CmsUser u')->where('u.username = ?', 'gblanco')->orWhere('u.name = ?', 'Guilherme'); + $this->assertEquals('DELETE FROM CmsUser u WHERE u.username = ? OR u.name = ?', $q->getDql()); + $q->free(); + } + +} diff --git a/tests/Orm/Query/IdentifierRecognitionTest.php b/tests/Orm/Query/IdentifierRecognitionTest.php new file mode 100755 index 000000000..02138bd9a --- /dev/null +++ b/tests/Orm/Query/IdentifierRecognitionTest.php @@ -0,0 +1,124 @@ +. + */ + +require_once 'lib/DoctrineTestInit.php'; + +/** + * Test case for testing the saving and referencing of query identifiers. + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @author Konsta Vesterinen + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + */ +class Orm_Query_IdentifierRecognitionTest extends Doctrine_OrmTestCase +{ + + public function testSingleAliasDeclarationIsSupported() + { + $query = new Doctrine_Query; + $query->setDql('SELECT u.* FROM CmsUser u'); + $parserResult = $query->parse(); + + $decl = $parserResult->getQueryComponent('u'); + + $this->assertTrue($decl['metadata'] instanceof Doctrine_ClassMetadata); + $this->assertEquals(null, $decl['relation']); + $this->assertEquals(null, $decl['parent']); + $this->assertEquals(null, $decl['agg']); + $this->assertEquals(null, $decl['map']); + } + + public function testSingleAliasDeclarationWithIndexByIsSupported() + { + $query = new Doctrine_Query; + $query->setDql('SELECT u.* FROM CmsUser u INDEX BY name'); + $parserResult = $query->parse(); + + $decl = $parserResult->getQueryComponent('u'); + + $this->assertTrue($decl['metadata'] instanceof Doctrine_ClassMetadata); + $this->assertEquals(null, $decl['relation']); + $this->assertEquals(null, $decl['parent']); + $this->assertEquals(null, $decl['agg']); + $this->assertEquals('name', $decl['map']); + } + + public function testQueryParserSupportsMultipleAliasDeclarations() + { + $query = new Doctrine_Query; + $query->setDql('SELECT u.* FROM CmsUser u INDEX BY name LEFT JOIN u.phonenumbers p'); + $parserResult = $query->parse(); + + $decl = $parserResult->getQueryComponent('u'); + + $this->assertTrue($decl['metadata'] instanceof Doctrine_ClassMetadata); + $this->assertEquals(null, $decl['relation']); + $this->assertEquals(null, $decl['parent']); + $this->assertEquals(null, $decl['agg']); + $this->assertEquals('name', $decl['map']); + + $decl = $parserResult->getQueryComponent('p'); + + $this->assertTrue($decl['metadata'] instanceof Doctrine_ClassMetadata); + $this->assertTrue($decl['relation'] instanceof Doctrine_Relation); + $this->assertEquals('u', $decl['parent']); + $this->assertEquals(null, $decl['agg']); + $this->assertEquals(null, $decl['map']); + } + + + public function testQueryParserSupportsMultipleAliasDeclarationsWithIndexBy() + { + $query = new Doctrine_Query; + $query->setDql('SELECT u.* FROM CmsUser u INDEX BY name LEFT JOIN u.articles a INNER JOIN u.phonenumbers pn INDEX BY phonenumber'); + $parserResult = $query->parse(); + + $decl = $parserResult->getQueryComponent('u'); + + $this->assertTrue($decl['metadata'] instanceof Doctrine_ClassMetadata); + $this->assertEquals(null, $decl['relation']); + $this->assertEquals(null, $decl['parent']); + $this->assertEquals(null, $decl['agg']); + $this->assertEquals('name', $decl['map']); + + $decl = $parserResult->getQueryComponent('a'); + + $this->assertTrue($decl['metadata'] instanceof Doctrine_ClassMetadata); + $this->assertTrue($decl['relation'] instanceof Doctrine_Relation); + $this->assertEquals('u', $decl['parent']); + $this->assertEquals(null, $decl['agg']); + $this->assertEquals(null, $decl['map']); + + $decl = $parserResult->getQueryComponent('pn'); + + $this->assertTrue($decl['metadata'] instanceof Doctrine_ClassMetadata); + $this->assertTrue($decl['relation'] instanceof Doctrine_Relation); + $this->assertEquals('u', $decl['parent']); + $this->assertEquals(null, $decl['agg']); + $this->assertEquals('phonenumber', $decl['map']); + } +} diff --git a/tests/Orm/Query/LanguageRecognitionTest.php b/tests/Orm/Query/LanguageRecognitionTest.php new file mode 100755 index 000000000..460ca70fd --- /dev/null +++ b/tests/Orm/Query/LanguageRecognitionTest.php @@ -0,0 +1,345 @@ +setDql($dql); + $parserResult = $query->parse(); + } catch (Doctrine_Exception $e) { + $this->fail($e->getMessage()); + } + } + + public function assertInvalidDql($dql) + { + try { + $query = new Doctrine_Query; + $query->setDql($dql); + $parserResult = $query->parse(); + + $this->fail('No syntax errors were detected, when syntax errors were expected'); + } catch (Doctrine_Exception $e) { + // It was expected! + } + } + + public function testEmptyQueryString() + { + $this->assertInvalidDql(''); + } + + public function testPlainFromClauseWithoutAlias() + { + $this->assertValidDql('SELECT * FROM CmsUser'); + } + + public function testPlainFromClauseWithAlias() + { + $this->assertValidDql('SELECT u.* FROM CmsUser u'); + } + + public function testSelectSingleComponentWithAsterisk() + { + $this->assertValidDql('SELECT u.* FROM CmsUser u'); + } + + public function testInvalidSelectSingleComponentWithAsterisk() + { + $this->assertValidDql('SELECT p.* FROM CmsUser u'); + } + + public function testSelectSingleComponentWithMultipleColumns() + { + $this->assertValidDql('SELECT u.name, u.username FROM CmsUser u'); + } + + public function testSelectMultipleComponentsWithAsterisk() + { + $this->assertValidDql('SELECT u.*, p.* FROM CmsUser u, u.phonenumbers p'); + } + + public function testSelectDistinctIsSupported() + { + $this->assertValidDql('SELECT DISTINCT u.name FROM CmsUser u'); + } + + public function testAggregateFunctionInSelect() + { + $this->assertValidDql('SELECT COUNT(u.id) FROM CmsUser u'); + } + + public function testAggregateFunctionWithDistinctInSelect() + { + $this->assertValidDql('SELECT COUNT(DISTINCT u.name) FROM CmsUser u'); + } + + public function testFunctionalExpressionsSupportedInWherePart() + { + $this->assertValidDql("SELECT u.name FROM CmsUser u WHERE TRIM(u.name) = 'someone'"); + } + + public function testArithmeticExpressionsSupportedInWherePart() + { + $this->assertValidDql('SELECT u.* FROM CmsUser u WHERE ((u.id + 5000) * u.id + 3) < 10000000'); + } + + public function testInExpressionSupportedInWherePart() + { + $this->assertValidDql('SELECT * FROM CmsUser WHERE CmsUser.id IN (1, 2)'); + } + + public function testNotInExpressionSupportedInWherePart() + { + $this->assertValidDql('SELECT * FROM CmsUser WHERE CmsUser.id NOT IN (1)'); + } + + public function testExistsExpressionSupportedInWherePart() + { + $this->assertValidDql('SELECT * FROM CmsUser u WHERE EXISTS (SELECT p.user_id FROM CmsPhonenumber p WHERE p.user_id = u.id)'); + } + + public function testNotExistsExpressionSupportedInWherePart() + { + $this->assertValidDql('SELECT * FROM CmsUser u WHERE NOT EXISTS (SELECT p.user_id FROM CmsPhonenumber p WHERE p.user_id = u.id)'); + } + + public function testLiteralValueAsInOperatorOperandIsSupported() + { + $this->assertValidDql('SELECT u.id FROM CmsUser u WHERE 1 IN (1, 2)'); + } + + public function testUpdateWorksWithOneColumn() + { + $this->assertValidDql("UPDATE CmsUser u SET u.name = 'someone'"); + } + + public function testUpdateWorksWithMultipleColumns() + { + $this->assertValidDql("UPDATE CmsUser u SET u.name = 'someone', u.username = 'some'"); + } + + public function testUpdateSupportsConditions() + { + $this->assertValidDql("UPDATE CmsUser u SET u.name = 'someone' WHERE u.id = 5"); + } + + public function testDeleteAll() + { + $this->assertValidDql('DELETE FROM CmsUser'); + } + + public function testDeleteWithCondition() + { + $this->assertValidDql('DELETE FROM CmsUser WHERE id = 3'); + } +/* + public function testDeleteWithLimit() + { + // LIMIT is not supported in DELETE + $this->assertValidDql('DELETE FROM CmsUser LIMIT 20'); + } + + public function testDeleteWithLimitAndOffset() + { + // LIMIT and OFFSET are not supported in DELETE + $this->assertValidDql('DELETE FROM CmsUser LIMIT 10 OFFSET 20'); + } +*/ + public function testAdditionExpression() + { + $this->assertValidDql('SELECT u.*, (u.id + u.id) addition FROM CmsUser u'); + } + + public function testSubtractionExpression() + { + $this->assertValidDql('SELECT u.*, (u.id - u.id) subtraction FROM CmsUser u'); + } + + public function testDivisionExpression() + { + $this->assertValidDql('SELECT u.*, (u.id/u.id) division FROM CmsUser u'); + } + + public function testMultiplicationExpression() + { + $this->assertValidDql('SELECT u.*, (u.id * u.id) multiplication FROM CmsUser u'); + } + + public function testNegationExpression() + { + $this->assertValidDql('SELECT u.*, -u.id negation FROM CmsUser u'); + } + + public function testExpressionWithPrecedingPlusSign() + { + $this->assertValidDql('SELECT u.*, +u.id FROM CmsUser u'); + } + + public function testAggregateFunctionInHavingClause() + { + $this->assertValidDql('SELECT u.name FROM CmsUser u LEFT JOIN u.phonenumbers p HAVING COUNT(p.phonenumber) > 2'); + $this->assertValidDql("SELECT u.name FROM CmsUser u LEFT JOIN u.phonenumbers p HAVING MAX(u.name) = 'zYne'"); + } + + public function testMultipleAggregateFunctionsInHavingClause() + { + $this->assertValidDql("SELECT u.name FROM CmsUser u LEFT JOIN u.phonenumbers p HAVING MAX(u.name) = 'zYne'"); + } + + public function testLeftJoin() + { + $this->assertValidDql('SELECT * FROM CmsUser u LEFT JOIN u.phonenumbers'); + } + + public function testJoin() + { + $this->assertValidDql('SELECT u.* FROM CmsUser u JOIN u.phonenumbers'); + } + + public function testInnerJoin() + { + $this->assertValidDql('SELECT * FROM CmsUser u INNER JOIN u.phonenumbers'); + } + + public function testMultipleLeftJoin() + { + $this->assertValidDql('SELECT * FROM CmsUser u LEFT JOIN u.articles LEFT JOIN u.phonenumbers'); + } + + public function testMultipleInnerJoin() + { + $this->assertValidDql('SELECT u.name FROM CmsUser u INNER JOIN u.articles INNER JOIN u.phonenumbers'); + } + + public function testMultipleInnerJoin2() + { + $this->assertValidDql('SELECT u.name FROM CmsUser u INNER JOIN u.articles, u.phonenumbers'); + } + + public function testMixingOfJoins() + { + $this->assertValidDql('SELECT u.name, a.topic, p.phonenumber FROM CmsUser u INNER JOIN u.articles a LEFT JOIN u.phonenumbers p'); + } + + public function testMixingOfJoins2() + { + $this->assertValidDql('SELECT u.name, u.articles.topic, c.text FROM CmsUser u INNER JOIN u.articles.comments c'); + } + + public function testOrderBySingleColumn() + { + $this->assertValidDql('SELECT u.name FROM CmsUser u ORDER BY u.name'); + } + + public function testOrderBySingleColumnAscending() + { + $this->assertValidDql('SELECT u.name FROM CmsUser u ORDER BY u.name ASC'); + } + + public function testOrderBySingleColumnDescending() + { + $this->assertValidDql('SELECT u.name FROM CmsUser u ORDER BY u.name DESC'); + } + + public function testOrderByMultipleColumns() + { + $this->assertValidDql('SELECT u.name, u.username FROM CmsUser u ORDER BY u.username DESC, u.name DESC'); + } + + public function testOrderByWithFunctionExpression() + { + $this->assertValidDql('SELECT u.name FROM CmsUser u ORDER BY COALESCE(u.id, u.name) DESC'); + } + + public function testSubselectInInExpression() + { + $this->assertValidDql("SELECT * FROM CmsUser u WHERE u.id NOT IN (SELECT u2.id FROM CmsUser u2 WHERE u2.name = 'zYne')"); + } +/* + public function testSubselectInSelectPart() + { + // Semantical error: Unknown query component u (probably in subselect) + $this->assertValidDql("SELECT u.name, (SELECT COUNT(p.phonenumber) FROM CmsPhonenumber p WHERE p.user_id = u.id) pcount FROM CmsUser u WHERE u.name = 'zYne' LIMIT 1"); + } +*/ + public function testPositionalInputParameter() + { + $this->assertValidDql('SELECT * FROM CmsUser u WHERE u.id = ?'); + } + + public function testNamedInputParameter() + { + $this->assertValidDql('SELECT * FROM CmsUser u WHERE u.id = :id'); + } +/* + public function testCustomJoinsAndWithKeywordSupported() + { + // We need existant classes here, otherwise semantical will always fail + $this->assertValidDql('SELECT c.*, c2.*, d.* FROM Record_Country c INNER JOIN c.City c2 WITH c2.id = 2 WHERE c.id = 1'); + } +*/ + public function testJoinConditionsSupported() + { + $this->assertValidDql("SELECT u.name, p.* FROM CmsUser u LEFT JOIN u.phonenumbers p ON p.phonenumber = '123 123'"); + } + + public function testIndexByClauseWithOneComponent() + { + $this->assertValidDql('SELECT * FROM CmsUser u INDEX BY name'); + } + + public function testIndexBySupportsJoins() + { + $this->assertValidDql('SELECT * FROM CmsUser u LEFT JOIN u.articles INDEX BY topic'); + } + + public function testIndexBySupportsJoins2() + { + $this->assertValidDql('SELECT * FROM CmsUser u INDEX BY name LEFT JOIN u.phonenumbers p INDEX BY phonenumber'); + } + + public function testBetweenExpressionSupported() + { + $this->assertValidDql("SELECT * FROM CmsUser u WHERE u.name BETWEEN 'jepso' AND 'zYne'"); + } + + public function testNotBetweenExpressionSupported() + { + $this->assertValidDql("SELECT * FROM CmsUser u WHERE u.name NOT BETWEEN 'jepso' AND 'zYne'"); + } +/* + public function testAllExpression() + { + // We need existant classes here, otherwise semantical will always fail + $this->assertValidDql('SELECT * FROM Employee e WHERE e.salary > ALL (SELECT m.salary FROM Manager m WHERE m.department = e.department)'); + } + + public function testAnyExpression() + { + // We need existant classes here, otherwise semantical will always fail + $this->assertValidDql('SELECT * FROM Employee e WHERE e.salary > ANY (SELECT m.salary FROM Manager m WHERE m.department = e.department)'); + } + + public function testSomeExpression() + { + // We need existant classes here, otherwise semantical will always fail + $this->assertValidDql('SELECT * FROM Employee e WHERE e.salary > SOME (SELECT m.salary FROM Manager m WHERE m.department = e.department)'); + } +*/ + public function testLikeExpression() + { + $this->assertValidDql("SELECT u.id FROM CmsUser u WHERE u.name LIKE 'z%'"); + } + + public function testNotLikeExpression() + { + $this->assertValidDql("SELECT u.id FROM CmsUser u WHERE u.name NOT LIKE 'z%'"); + } + + public function testLikeExpressionWithCustomEscapeCharacter() + { + $this->assertValidDql("SELECT u.id FROM CmsUser u WHERE u.name LIKE 'z|%' ESCAPE '|'"); + } +} diff --git a/tests/Orm/Query/ScannerTest.php b/tests/Orm/Query/ScannerTest.php new file mode 100755 index 000000000..b092a3539 --- /dev/null +++ b/tests/Orm/Query/ScannerTest.php @@ -0,0 +1,280 @@ +next(); + $this->assertEquals(Doctrine_Query_Token::T_IDENTIFIER, $token['type']); + $this->assertEquals('u', $token['value']); + } + + public function testScannerRecognizesIdentifierConsistingOfLetters() + { + $scanner = new Doctrine_Query_Scanner('someIdentifier'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_IDENTIFIER, $token['type']); + $this->assertEquals('someIdentifier', $token['value']); + } + + public function testScannerRecognizesIdentifierIncludingDigits() + { + $scanner = new Doctrine_Query_Scanner('s0m31d3nt1f13r'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_IDENTIFIER, $token['type']); + $this->assertEquals('s0m31d3nt1f13r', $token['value']); + } + + public function testScannerRecognizesIdentifierIncludingUnderscore() + { + $scanner = new Doctrine_Query_Scanner('some_identifier'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_IDENTIFIER, $token['type']); + $this->assertEquals('some_identifier', $token['value']); + } + + public function testScannerRecognizesDecimalInteger() + { + $scanner = new Doctrine_Query_Scanner('1234'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_INTEGER, $token['type']); + $this->assertEquals(1234, $token['value']); + } + + public function testScannerRecognizesFloat() + { + $scanner = new Doctrine_Query_Scanner('1.234'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(1.234, $token['value']); + } + + public function testScannerRecognizesFloatWithExponent() + { + $scanner = new Doctrine_Query_Scanner('1.2e3'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(1.2e3, $token['value']); + } + + public function testScannerRecognizesFloatWithExponent2() + { + $scanner = new Doctrine_Query_Scanner('0.2e3'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(.2e3, $token['value']); + } + + public function testScannerRecognizesFloatWithNegativeExponent() + { + $scanner = new Doctrine_Query_Scanner('7E-10'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(7E-10, $token['value']); + } + + public function testScannerRecognizesFloatBig() + { + $scanner = new Doctrine_Query_Scanner('1,234,567.89'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(1.23456789e6, $token['value']); + } + + public function testScannerRecognizesFloatBigWrongPoint() + { + $scanner = new Doctrine_Query_Scanner('12,34,56,7.89'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(1.23456789e6, $token['value']); + } + + public function testScannerRecognizesFloatLocaleSpecific() + { + $scanner = new Doctrine_Query_Scanner('1,234'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(1.234, $token['value']); + } + + public function testScannerRecognizesFloatLocaleSpecificBig() + { + $scanner = new Doctrine_Query_Scanner('1.234.567,89'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(1.23456789e6, $token['value']); + } + + public function testScannerRecognizesFloatLocaleSpecificBigWrongPoint() + { + $scanner = new Doctrine_Query_Scanner('12.34.56.7,89'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(1.23456789e6, $token['value']); + } + + public function testScannerRecognizesFloatLocaleSpecificExponent() + { + $scanner = new Doctrine_Query_Scanner('1,234e2'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(1.234e2, $token['value']); + } + + public function testScannerRecognizesFloatLocaleSpecificExponent2() + { + $scanner = new Doctrine_Query_Scanner('0,234e2'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertEquals(.234e2, $token['value']); + } + + public function testScannerRecognizesFloatContainingWhitespace() + { + $scanner = new Doctrine_Query_Scanner('- 1.234e2'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_NONE, $token['type']); + $this->assertEquals('-', $token['value']); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_FLOAT, $token['type']); + $this->assertNotEquals(-1.234e2, $token['value']); + $this->assertEquals(1.234e2, $token['value']); + } + + public function testScannerRecognizesStringContainingWhitespace() + { + $scanner = new Doctrine_Query_Scanner("'This is a string.'"); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_STRING, $token['type']); + $this->assertEquals("'This is a string.'", $token['value']); + } + + public function testScannerRecognizesStringContainingSingleQuotes() + { + $scanner = new Doctrine_Query_Scanner("'abc''defg'''"); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_STRING, $token['type']); + $this->assertEquals("'abc''defg'''", $token['value']); + } + + public function testScannerRecognizesInputParameter() + { + $scanner = new Doctrine_Query_Scanner('?'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_INPUT_PARAMETER, $token['type']); + $this->assertEquals('?', $token['value']); + } + + public function testScannerRecognizesNamedInputParameter() + { + $scanner = new Doctrine_Query_Scanner(':name'); + + $token = $scanner->next(); + $this->assertEquals(Doctrine_Query_Token::T_INPUT_PARAMETER, $token['type']); + $this->assertEquals(':name', $token['value']); + } + + public function testScannerTokenizesASimpleQueryCorrectly() + { + $dql = "SELECT u.* FROM User u WHERE u.name = 'Jack O''Neil'"; + $scanner = new Doctrine_Query_Scanner($dql); + + $tokens = array( + array( + 'value' => 'SELECT', + 'type' => Doctrine_Query_Token::T_SELECT, + 'position' => 0 + ), + array( + 'value' => 'u', + 'type' => Doctrine_Query_Token::T_IDENTIFIER, + 'position' => 7 + ), + array( + 'value' => '.', + 'type' => Doctrine_Query_Token::T_NONE, + 'position' => 8 + ), + array( + 'value' => '*', + 'type' => Doctrine_Query_Token::T_NONE, + 'position' => 9 + ), + array( + 'value' => 'FROM', + 'type' => Doctrine_Query_Token::T_FROM, + 'position' => 11 + ), + array( + 'value' => 'User', + 'type' => Doctrine_Query_Token::T_IDENTIFIER, + 'position' => 16 + ), + array( + 'value' => 'u', + 'type' => Doctrine_Query_Token::T_IDENTIFIER, + 'position' => 21 + ), + array( + 'value' => 'WHERE', + 'type' => Doctrine_Query_Token::T_WHERE, + 'position' => 23 + ), + array( + 'value' => 'u', + 'type' => Doctrine_Query_Token::T_IDENTIFIER, + 'position' => 29 + ), + array( + 'value' => '.', + 'type' => Doctrine_Query_Token::T_NONE, + 'position' => 30 + ), + array( + 'value' => 'name', + 'type' => Doctrine_Query_Token::T_IDENTIFIER, + 'position' => 31 + ), + array( + 'value' => '=', + 'type' => Doctrine_Query_Token::T_NONE, + 'position' => 36 + ), + array( + 'value' => "'Jack O''Neil'", + 'type' => Doctrine_Query_Token::T_STRING, + 'position' => 38 + ) + ); + + foreach ($tokens as $expected) { + $actual = $scanner->next(); + $this->assertEquals($expected['value'], $actual['value']); + $this->assertEquals($expected['type'], $actual['type']); + $this->assertEquals($expected['position'], $actual['position']); + } + + $this->assertNull($scanner->next()); + } +} diff --git a/tests/Orm/Query/SelectSqlGenerationTest.php b/tests/Orm/Query/SelectSqlGenerationTest.php new file mode 100755 index 000000000..095233a6f --- /dev/null +++ b/tests/Orm/Query/SelectSqlGenerationTest.php @@ -0,0 +1,74 @@ +. + */ +require_once 'lib/DoctrineTestInit.php'; +/** + * Test case for testing the saving and referencing of query identifiers. + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @author Konsta Vesterinen + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + * @todo 1) [romanb] We might want to split the SQL generation tests into multiple + * testcases later since we'll have a lot of them and we might want to have special SQL + * generation tests for some dbms specific SQL syntaxes. + */ +class Orm_Query_SelectSqlGenerationTest extends Doctrine_OrmTestCase +{ + public function testWithoutWhere() + { + $q = new Doctrine_Query(); + + // NO WhereClause + $q->setDql('SELECT u.id FROM CmsUser u'); + $this->assertEquals('SELECT cu.id AS cu__id FROM cms_user cu WHERE 1 = 1', $q->getSql()); + $q->free(); + + //$q->setDql('SELECT u.* FROM CmsUser u'); + //$this->assertEquals('DELETE FROM cms_user cu WHERE 1 = 1', $q->getSql()); + //$q->free(); + } + +/* + public function testWithWhere() + { + $q = new Doctrine_Query(); + + // "WHERE" ConditionalExpression + // ConditionalExpression = ConditionalTerm {"OR" ConditionalTerm} + // ConditionalTerm = ConditionalFactor {"AND" ConditionalFactor} + // ConditionalFactor = ["NOT"] ConditionalPrimary + // ConditionalPrimary = SimpleConditionalExpression | "(" ConditionalExpression ")" + // SimpleConditionalExpression + // = Expression (ComparisonExpression | BetweenExpression | LikeExpression + // | InExpression | NullComparisonExpression) | ExistsExpression + + // If this one test fail, all others will fail too. That's the simplest case possible + $q->setDql('SELECT u.* FROM CmsUser u WHERE id = ?'); + $this->assertEquals('DELETE FROM cms_user cu WHERE cu.id = ?', $q->getSql()); + $q->free(); + } +*/ +} \ No newline at end of file diff --git a/lib/Doctrine/Query/Set.php b/tests/Orm/Query/UpdateSqlGenerationTest.php old mode 100644 new mode 100755 similarity index 54% rename from lib/Doctrine/Query/Set.php rename to tests/Orm/Query/UpdateSqlGenerationTest.php index 5edc0b909..dda959dcd --- a/lib/Doctrine/Query/Set.php +++ b/tests/Orm/Query/UpdateSqlGenerationTest.php @@ -1,60 +1,53 @@ -. - */ -Doctrine::autoload('Doctrine_Query_Part'); -/** - * Doctrine_Query - * - * @package Doctrine - * @subpackage Query - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL - * @link www.phpdoctrine.org - * @since 1.0 - * @version $Revision$ - * @author Konsta Vesterinen - */ -class Doctrine_Query_Set extends Doctrine_Query_Part -{ - public function parse($dql) - { - $terms = $this->_tokenizer->sqlExplode($dql, ' '); - - foreach ($terms as $term) { - - preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $term, $m); - - if (isset($m[0])) { - foreach ($m[0] as $part) { - $e = explode('.', trim($part)); - $field = array_pop($e); - - $reference = implode('.', $e); - - $alias = $this->query->getTableAlias($reference); - $map = $this->query->getAliasDeclaration($reference); - - $dql = str_replace($part, $map['table']->getColumnName($field), $dql); - } - } - } - - return $dql; - } -} \ No newline at end of file +. + */ + +/** + * Test case for testing the saving and referencing of query identifiers. + * + * @package Doctrine + * @subpackage Query + * @author Guilherme Blanco + * @author Janne Vanhala + * @author Konsta Vesterinen + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 1.0 + * @version $Revision$ + * @todo 1) [romanb] We might want to split the SQL generation tests into multiple + * testcases later since we'll have a lot of them and we might want to have special SQL + * generation tests for some dbms specific SQL syntaxes. + */ +class Orm_Query_UpdateSqlGenerationTest extends Doctrine_OrmTestCase +{ + public function testWithoutWhere() + { + $q = new Doctrine_Query(); + + // NO WhereClause + $q->setDql('UPDATE CmsUser u SET name = ?'); + $this->assertEquals('UPDATE cms_user cu SET cu.name = ? WHERE 1 = 1', $q->getSql()); + $q->free(); + + $q->setDql('UPDATE CmsUser u SET name = ?, username = ?'); + $this->assertEquals('UPDATE cms_user cu SET cu.name = ?, cu.username = ? WHERE 1 = 1', $q->getSql()); + $q->free(); + } +} diff --git a/tests/lib/Doctrine_OrmTestCase.php b/tests/lib/Doctrine_OrmTestCase.php index 3b2fbd9f5..d044450be 100644 --- a/tests/lib/Doctrine_OrmTestCase.php +++ b/tests/lib/Doctrine_OrmTestCase.php @@ -5,5 +5,7 @@ */ class Doctrine_OrmTestCase extends Doctrine_TestCase { - -} \ No newline at end of file + protected function setUp() { + $em = new Doctrine_EntityManager(new Doctrine_Connection_Mock()); + } +} diff --git a/tests/models/cms/CmsArticle.php b/tests/models/cms/CmsArticle.php old mode 100644 new mode 100755 diff --git a/tests/models/cms/CmsComment.php b/tests/models/cms/CmsComment.php old mode 100644 new mode 100755 diff --git a/tests/models/cms/CmsPhonenumber.php b/tests/models/cms/CmsPhonenumber.php old mode 100644 new mode 100755 diff --git a/tests/models/forum/ForumBoard.php b/tests/models/forum/ForumBoard.php old mode 100644 new mode 100755 diff --git a/tests/models/forum/ForumCategory.php b/tests/models/forum/ForumCategory.php old mode 100644 new mode 100755