diff --git a/composer.json b/composer.json index 53cc918..dc49a86 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ ], "require": { "php": ">=5.3.0", - "phpcr/phpcr": "~2.1.0-beta10", + "phpcr/phpcr": "~2.1.0-RC1", "symfony/console": "~2.0" }, "autoload": { diff --git a/src/PHPCR/Util/CND/Scanner/AbstractScanner.php b/src/PHPCR/Util/CND/Scanner/AbstractScanner.php index 4824ccb..5e598bc 100644 --- a/src/PHPCR/Util/CND/Scanner/AbstractScanner.php +++ b/src/PHPCR/Util/CND/Scanner/AbstractScanner.php @@ -41,7 +41,7 @@ public function applyFilters(Token $token) $token = $filter->filter($token); - if (is_null($token)) { + if (null === $token) { break; } } diff --git a/src/PHPCR/Util/QOM/QomToSql2QueryConverter.php b/src/PHPCR/Util/QOM/QomToSql2QueryConverter.php index 2a28bd5..1dcae72 100644 --- a/src/PHPCR/Util/QOM/QomToSql2QueryConverter.php +++ b/src/PHPCR/Util/QOM/QomToSql2QueryConverter.php @@ -116,7 +116,7 @@ protected function convertSameNodeJoinCondition(QOM\SameNodeJoinConditionInterfa return $this->generator->evalSameNodeJoinCondition( $condition->getSelector1Name(), $condition->getSelector2Name(), - ! is_null($condition->getSelector2Path()) ? $this->convertPath($condition->getSelector2Path()) : null); + null !== $condition->getSelector2Path() ? $this->convertPath($condition->getSelector2Path()) : null); } /** diff --git a/src/PHPCR/Util/QOM/QueryBuilder.php b/src/PHPCR/Util/QOM/QueryBuilder.php index 8ebf16d..c0847ff 100644 --- a/src/PHPCR/Util/QOM/QueryBuilder.php +++ b/src/PHPCR/Util/QOM/QueryBuilder.php @@ -352,16 +352,16 @@ public function setColumns(array $columns) * Identifies a property in the specified or default selector to include in the tabular view of query results. * Replaces any previously specified columns to be selected if any. * + * @param string $selectorName * @param string $propertyName * @param string $columnName - * @param string $selectorName * * @return QueryBuilder This QueryBuilder instance. */ - public function select($propertyName, $columnName = null, $selectorName = null) + public function select($selectorName, $propertyName, $columnName = null) { $this->state = self::STATE_DIRTY; - $this->columns = array($this->qomFactory->column($propertyName, $columnName, $selectorName)); + $this->columns = array($this->qomFactory->column($selectorName, $propertyName, $columnName)); return $this; } @@ -369,16 +369,16 @@ public function select($propertyName, $columnName = null, $selectorName = null) /** * Adds a property in the specified or default selector to include in the tabular view of query results. * + * @param string $selectorName * @param string $propertyName * @param string $columnName - * @param string $selectorName * * @return QueryBuilder This QueryBuilder instance. */ - public function addSelect($propertyName, $columnName = null, $selectorName = null) + public function addSelect($selectorName, $propertyName, $columnName = null) { $this->state = self::STATE_DIRTY; - $this->columns[] = $this->qomFactory->column($propertyName, $columnName, $selectorName); + $this->columns[] = $this->qomFactory->column($selectorName, $propertyName, $columnName); return $this; } diff --git a/src/PHPCR/Util/QOM/Sql2Generator.php b/src/PHPCR/Util/QOM/Sql2Generator.php index ab94cc2..6053e12 100644 --- a/src/PHPCR/Util/QOM/Sql2Generator.php +++ b/src/PHPCR/Util/QOM/Sql2Generator.php @@ -17,8 +17,11 @@ class Sql2Generator extends BaseSqlGenerator * Selector ::= nodeTypeName ['AS' selectorName] * nodeTypeName ::= Name * - * @param string $nodeTypeName The node type of the selector. If it does not contain starting and ending brackets ([]) they will be added automatically - * @param string $selectorName + * @param string $nodeTypeName The node type of the selector. If it + * does not contain starting and ending brackets ([]) they will be + * added automatically. + * @param string|null $selectorName The selector name. If it is different than + * the nodeTypeName, the alias is declared. * * @return string */ @@ -26,9 +29,9 @@ public function evalSelector($nodeTypeName, $selectorName = null) { $sql2 = $this->addBracketsIfNeeded($nodeTypeName); - $name = $selectorName; - if (! is_null($name)) { - $sql2 .= ' AS ' . $name; + if (null !== $selectorName && $nodeTypeName !== $selectorName) { + // if the selector name is the same as the type name, this is implicit for sql2 + $sql2 .= ' AS ' . $selectorName; } return $sql2; @@ -112,7 +115,9 @@ public function evalSameNodeJoinCondition($sel1Name, $sel2Name, $sel2Path = null . $this->addBracketsIfNeeded($sel1Name) . ', ' . $this->addBracketsIfNeeded($sel2Name) ; - $sql2 .= ! is_null($sel2Path) ? ', ' . $sel2Path : ''; + if (null !== $sel2Path) { + $sql2 .= ', ' . $sel2Path; + } $sql2 .= ')'; return $sql2; @@ -165,7 +170,7 @@ public function evalDescendantNodeJoinCondition($descendantSelectorName, $ancest public function evalSameNode($path, $selectorName = null) { $sql2 = 'ISSAMENODE('; - $sql2 .= is_null($selectorName) ? $path : $this->addBracketsIfNeeded($selectorName) . ', ' . $path; + $sql2 .= null === $selectorName ? $path : $this->addBracketsIfNeeded($selectorName) . ', ' . $path; $sql2 .= ')'; return $sql2; @@ -180,7 +185,7 @@ public function evalSameNode($path, $selectorName = null) public function evalChildNode($path, $selectorName = null) { $sql2 = 'ISCHILDNODE('; - $sql2 .= is_null($selectorName) ? $path : $this->addBracketsIfNeeded($selectorName) . ', ' . $path; + $sql2 .= null === $selectorName ? $path : $this->addBracketsIfNeeded($selectorName) . ', ' . $path; $sql2 .= ')'; return $sql2; @@ -195,7 +200,7 @@ public function evalChildNode($path, $selectorName = null) public function evalDescendantNode($path, $selectorName = null) { $sql2 = 'ISDESCENDANTNODE('; - $sql2 .= is_null($selectorName) ? $path : $this->addBracketsIfNeeded($selectorName) . ', ' . $path; + $sql2 .= null === $selectorName ? $path : $this->addBracketsIfNeeded($selectorName) . ', ' . $path; $sql2 .= ')'; return $sql2; @@ -289,7 +294,7 @@ public function evalFullTextSearchScore($selectorValue = null) */ public function evalPropertyValue($propertyName, $selectorName = null) { - $sql2 = ! is_null($selectorName) ? $this->addBracketsIfNeeded($selectorName) . '.' : ''; + $sql2 = null !== $selectorName ? $this->addBracketsIfNeeded($selectorName) . '.' : ''; if (false !== strpos($propertyName, ':')) { $propertyName = "[$propertyName]"; } @@ -340,11 +345,14 @@ public function evalColumns($columns) public function evalColumn($selectorName, $propertyName = null, $colname = null) { $sql2 = ''; - if (! is_null($selectorName) && is_null($propertyName) && is_null($colname)) { + if (null !== $selectorName && null === $propertyName && null === $colname) { $sql2 .= $this->addBracketsIfNeeded($selectorName) . '.*'; } else { $sql2 .= $this->evalPropertyValue($propertyName, $selectorName); - $sql2 .= ! is_null($colname) ? ' AS ' . $colname : ''; + if (null !== $colname && $colname !== $propertyName) { + // if the column name is the same as the property name, this is implicit for sql2 + $sql2 .= ' AS ' . $colname; + } } return $sql2; diff --git a/src/PHPCR/Util/QOM/Sql2Scanner.php b/src/PHPCR/Util/QOM/Sql2Scanner.php index dff3d3a..74aa1b9 100644 --- a/src/PHPCR/Util/QOM/Sql2Scanner.php +++ b/src/PHPCR/Util/QOM/Sql2Scanner.php @@ -2,6 +2,8 @@ namespace PHPCR\Util\QOM; +use PHPCR\Query\InvalidQueryException; + /** * Split an SQL2 statement into string tokens. Allows lookup and fetching of tokens. * @@ -104,7 +106,7 @@ public function expectToken($token, $case_insensitive = true) { $nextToken = $this->fetchNextToken(); if (! $this->tokenIs($nextToken, $token, $case_insensitive)) { - throw new \Exception("Syntax error: Expected $token, found $nextToken"); + throw new InvalidQueryException("Syntax error: Expected '$token', found '$nextToken' in {$this->sql2}"); } } diff --git a/src/PHPCR/Util/QOM/Sql2ToQomQueryConverter.php b/src/PHPCR/Util/QOM/Sql2ToQomQueryConverter.php index b0dfbb0..dc26ade 100644 --- a/src/PHPCR/Util/QOM/Sql2ToQomQueryConverter.php +++ b/src/PHPCR/Util/QOM/Sql2ToQomQueryConverter.php @@ -2,8 +2,26 @@ namespace PHPCR\Util\QOM; -use PHPCR\Query\QOM; +use PHPCR\Query\InvalidQueryException; +use PHPCR\Query\QOM\ChildNodeJoinConditionInterface; +use PHPCR\Query\QOM\ColumnInterface; +use PHPCR\Query\QOM\ComparisonInterface; +use PHPCR\Query\QOM\ConstraintInterface; +use PHPCR\Query\QOM\DescendantNodeJoinConditionInterface; +use PHPCR\Query\QOM\DynamicOperandInterface; +use PHPCR\Query\QOM\EquiJoinConditionInterface; +use PHPCR\Query\QOM\FullTextSearchInterface; +use PHPCR\Query\QOM\JoinConditionInterface; +use PHPCR\Query\QOM\LiteralInterface; +use PHPCR\Query\QOM\NotInterface; +use PHPCR\Query\QOM\PropertyExistenceInterface; +use PHPCR\Query\QOM\PropertyValueInterface; use PHPCR\Query\QOM\QueryObjectModelConstantsInterface as Constants; +use PHPCR\Query\QOM\QueryObjectModelFactoryInterface; +use PHPCR\Query\QOM\QueryObjectModelInterface; +use PHPCR\Query\QOM\SameNodeJoinConditionInterface; +use PHPCR\Query\QOM\SourceInterface; +use PHPCR\Query\QOM\StaticOperandInterface; /** * Parse SQL2 statements and output a corresponding QOM objects tree. @@ -16,14 +34,14 @@ class Sql2ToQomQueryConverter /** * The factory to create QOM objects * - * @var \PHPCR\Query\QOM\QueryObjectModelFactoryInterface + * @var QueryObjectModelFactoryInterface */ protected $factory; /** * Scanner to parse SQL2 * - * @var \PHPCR\Util\QOM\Sql2Scanner; + * @var Sql2Scanner; */ protected $scanner; @@ -34,12 +52,22 @@ class Sql2ToQomQueryConverter */ protected $sql2; + /** + * The selector is not required for SQL2 but for QOM. + * + * We keep all selectors we encounter. If there is exactly one, it is used + * whenever we encounter non-qualified names. + * + * @var string|array + */ + protected $implicitSelectorName = null; + /** * Instantiate a converter * - * @param \PHPCR\Query\QOM\QueryObjectModelFactoryInterface $factory + * @param QueryObjectModelFactoryInterface $factory */ - public function __construct(QOM\QueryObjectModelFactoryInterface $factory) + public function __construct(QueryObjectModelFactoryInterface $factory) { $this->factory = $factory; } @@ -50,44 +78,46 @@ public function __construct(QOM\QueryObjectModelFactoryInterface $factory) * * @param string $sql2 * - * @return \PHPCR\Query\QOM\QueryObjectModelInterface; + * @return QueryObjectModelInterface */ public function parse($sql2) { + $this->implicitSelectorName = null; $this->sql2 = $sql2; $this->scanner = new Sql2Scanner($sql2); $source = null; - $columns = array(); + $columnData = array(); $constraint = null; $orderings = array(); while ($this->scanner->lookupNextToken() !== '') { switch (strtoupper($this->scanner->lookupNextToken())) { + case 'SELECT': + $columnData = $this->scanColumns(); + break; case 'FROM': $source = $this->parseSource(); break; - case 'SELECT': - $columns = $this->parseColumns(); + case 'WHERE': + $this->scanner->expectToken('WHERE'); + $constraint = $this->parseConstraint(); break; case 'ORDER': // Ordering, check there is a BY $this->scanner->expectTokens(array('ORDER', 'BY')); $orderings = $this->parseOrderings(); break; - case 'WHERE': - $this->scanner->expectToken('WHERE'); - $constraint = $this->parseConstraint(); - break; default: - // Exit loop for debugging - break(2); + throw new InvalidQueryException('Expected end of query, got ' . $this->scanner->lookupNextToken() . ' in ' . $this->sql2); } } - if (!$source instanceof \PHPCR\Query\QOM\SourceInterface) { - throw new \PHPCR\Query\InvalidQueryException('Invalid query, source could not be determined: '.$sql2); + if (!$source instanceof SourceInterface) { + throw new InvalidQueryException('Invalid query, source could not be determined: '.$sql2); } + $columns = $this->buildColumns($columnData); + $query = $this->factory->createQuery($source, $constraint, $orderings, $columns); return $query; @@ -97,7 +127,7 @@ public function parse($sql2) * 6.7.2. Source * Parse an SQL2 source definition and return the corresponding QOM Source * - * @return \PHPCR\Query\QOM\SourceInterface + * @return SourceInterface */ protected function parseSource() { @@ -121,16 +151,18 @@ protected function parseSource() */ protected function parseSelector() { - $token = $this->fetchTokenWithoutBrackets(); + $nodetype = $this->fetchTokenWithoutBrackets(); if ($this->scanner->tokenIs($this->scanner->lookupNextToken(), 'AS')) { $this->scanner->fetchNextToken(); // Consume the AS $selectorName = $this->parseName(); + $this->updateImplicitSelectorName($selectorName); - return $this->factory->selector($token, $selectorName); + return $this->factory->selector($selectorName, $nodetype); } + $this->updateImplicitSelectorName($nodetype); - return $this->factory->selector($token); + return $this->factory->selector($nodetype, $nodetype); } /** @@ -140,7 +172,6 @@ protected function parseSelector() */ protected function parseName() { - // TODO: check it's the correct way to parse a JCR name return $this->scanner->fetchNextToken(); } @@ -187,7 +218,7 @@ protected function parseJoinType() $joinType = Constants::JCR_JOIN_TYPE_RIGHT_OUTER; break; default: - throw new \Exception("Syntax error: Expected JOIN, INNER JOIN, RIGHT JOIN or LEFT JOIN in '{$this->sql2}'"); + throw new InvalidQueryException("Syntax error: Expected JOIN, INNER JOIN, RIGHT JOIN or LEFT JOIN in '{$this->sql2}'"); } return $joinType; @@ -195,9 +226,9 @@ protected function parseJoinType() /** * 6.7.7. JoinCondition - * Parse an SQL2 join condition and return a QOM\Joincondition + * Parse an SQL2 join condition and return a JoinConditionInterface * - * @return \PHPCR\Query\QOM\JoinConditionInterface + * @return JoinConditionInterface */ protected function parseJoinCondition() { @@ -221,31 +252,31 @@ protected function parseJoinCondition() /** * 6.7.8. EquiJoinCondition - * Parse an SQL2 equijoin condition and return a QOM\EquiJoinCondition + * Parse an SQL2 equijoin condition and return a EquiJoinConditionInterface * - * @return \PHPCR\Query\QOM\EquiJoinConditionInterface + * @return EquiJoinConditionInterface */ protected function parseEquiJoin() { - list($prop1, $selector1) = $this->parseIdentifier(); + list($selectorName1, $prop1) = $this->parseIdentifier(); $this->scanner->expectToken('='); - list($prop2, $selector2) = $this->parseIdentifier(); + list($selectorName2, $prop2) = $this->parseIdentifier(); - return $this->factory->equiJoinCondition($selector1, $prop1, $selector2, $prop2); + return $this->factory->equiJoinCondition($selectorName1, $prop1, $selectorName2, $prop2); } /** * 6.7.9 SameNodeJoinCondition - * Parse an SQL2 same node join condition and return a QOM\SameNodeJoinCondition + * Parse an SQL2 same node join condition and return a SameNodeJoinConditionInterface * - * @return \PHPCR\Query\QOM\SameNodeJoinConditionInterface + * @return SameNodeJoinConditionInterface */ protected function parseSameNodeJoinCondition() { $this->scanner->expectTokens(array('ISSAMENODE', '(')); - $selector1 = $this->fetchTokenWithoutBrackets(); + $selectorName1 = $this->fetchTokenWithoutBrackets(); $this->scanner->expectToken(','); - $selector2 = $this->fetchTokenWithoutBrackets(); + $selectorName2 = $this->fetchTokenWithoutBrackets(); $token = $this->scanner->lookupNextToken(); if ($this->scanner->tokenIs($token, ',')) { @@ -257,14 +288,14 @@ protected function parseSameNodeJoinCondition() $this->scanner->expectToken(')'); - return $this->factory->sameNodeJoinCondition($selector1, $selector2, $path); + return $this->factory->sameNodeJoinCondition($selectorName1, $selectorName2, $path); } /** * 6.7.10 ChildNodeJoinCondition - * Parse an SQL2 child node join condition and return a QOM\ChildNodeJoinCondition + * Parse an SQL2 child node join condition and return a ChildNodeJoinConditionInterface * - * @return \PHPCR\Query\QOM\ChildNodeJoinConditionInterface + * @return ChildNodeJoinConditionInterface */ protected function parseChildNodeJoinCondition() { @@ -279,26 +310,26 @@ protected function parseChildNodeJoinCondition() /** * 6.7.11 DescendantNodeJoinCondition - * Parse an SQL2 descendant node join condition and return a QOM\DescendantNodeJoinCondition + * Parse an SQL2 descendant node join condition and return a DescendantNodeJoinConditionInterface * - * @return \PHPCR\Query\QOM\DescendantNodeJoinConditionInterface + * @return DescendantNodeJoinConditionInterface */ protected function parseDescendantNodeJoinCondition() { $this->scanner->expectTokens(array('ISDESCENDANTNODE', '(')); - $child = $this->fetchTokenWithoutBrackets(); + $descendant = $this->fetchTokenWithoutBrackets(); $this->scanner->expectToken(','); $parent = $this->fetchTokenWithoutBrackets(); $this->scanner->expectToken(')'); - return $this->factory->descendantNodeJoinCondition($child, $parent); + return $this->factory->descendantNodeJoinCondition($descendant, $parent); } /** * 6.7.13 And * 6.7.14 Or * - * @return \PHPCR\Query\QOM\ConstraintInterface + * @return ConstraintInterface */ protected function parseConstraint($lhs = null, $minprec = 0) { @@ -349,7 +380,7 @@ protected function parseConstraint($lhs = null, $minprec = 0) /** * 6.7.12 Constraint * - * @return \PHPCR\Query\QOM\ConstraintInterface + * @return ConstraintInterface */ protected function parsePrimaryConstraint() { @@ -396,7 +427,7 @@ protected function parsePrimaryConstraint() // No constraint read, if ($constraint === null) { - throw new \Exception("Syntax error: constraint expected in '{$this->sql2}'"); + throw new InvalidQueryException("Syntax error: constraint expected in '{$this->sql2}'"); } return $constraint; @@ -405,7 +436,7 @@ protected function parsePrimaryConstraint() /** * 6.7.15 Not * - * @return \PHPCR\Query\QOM\NotInterface + * @return NotInterface */ protected function parseNot() { @@ -417,14 +448,14 @@ protected function parseNot() /** * 6.7.16 Comparison * - * @return \PHPCR\Query\QOM\ComparisonInterface + * @return ComparisonInterface */ protected function parseComparison() { $op1 = $this->parseDynamicOperand(); if (null === $op1) { - throw new \Exception("Syntax error: dynamic operator expected in '{$this->sql2}'"); + throw new InvalidQueryException("Syntax error: dynamic operator expected in '{$this->sql2}'"); } $operator = $this->parseOperator(); @@ -458,46 +489,46 @@ protected function parseOperator() return Constants::JCR_OPERATOR_LIKE; } - throw new \Exception("Syntax error: operator expected in '{$this->sql2}'"); + throw new InvalidQueryException("Syntax error: operator expected in '{$this->sql2}'"); } /** * 6.7.18 PropertyExistence * - * @return \PHPCR\Query\QOM\PropertyExistenceInterface + * @return PropertyExistenceInterface */ protected function parsePropertyExistence() { - list($prop, $selector) = $this->parseIdentifier(); + list($selectorName, $prop) = $this->parseIdentifier(); $this->scanner->expectToken('IS'); $token = $this->scanner->lookupNextToken(); if ($this->scanner->tokenIs($token, 'NULL')) { $this->scanner->fetchNextToken(); - return $this->factory->notConstraint($this->factory->propertyExistence($prop, $selector)); + return $this->factory->notConstraint($this->factory->propertyExistence($selectorName, $prop)); } $this->scanner->expectTokens(array('NOT', 'NULL')); - return $this->factory->propertyExistence($prop, $selector); + return $this->factory->propertyExistence($selectorName, $prop); } /** * 6.7.19 FullTextSearch * - * @return \PHPCR\Query\QOM\FullTextSearchInterface + * @return FullTextSearchInterface */ protected function parseFullTextSearch() { $this->scanner->expectTokens(array('CONTAINS', '(')); - list($propertyName, $selectorName) = $this->parseIdentifier(); + list($selectorName, $propertyName) = $this->parseIdentifier(); $this->scanner->expectToken(','); $expression = $this->parseLiteral()->getLiteralValue(); $this->scanner->expectToken(')'); - return $this->factory->fullTextSearch($propertyName, $expression, $selectorName); + return $this->factory->fullTextSearch($selectorName, $propertyName, $expression); } /** @@ -507,16 +538,16 @@ protected function parseSameNode() { $this->scanner->expectTokens(array('ISSAMENODE', '(')); if ($this->scanner->tokenIs($this->scanner->lookupNextToken(1), ',')) { - $selector = $this->scanner->fetchNextToken(); + $selectorName = $this->scanner->fetchNextToken(); $this->scanner->expectToken(','); $path = $this->parsePath(); } else { - $selector = null; + $selectorName = $this->implicitSelectorName; $path = $this->parsePath(); } $this->scanner->expectToken(')'); - return $this->factory->sameNode($path, $selector); + return $this->factory->sameNode($selectorName, $path); } /** @@ -526,16 +557,16 @@ protected function parseChildNode() { $this->scanner->expectTokens(array('ISCHILDNODE', '(')); if ($this->scanner->tokenIs($this->scanner->lookupNextToken(1), ',')) { - $selector = $this->scanner->fetchNextToken(); + $selectorName = $this->scanner->fetchNextToken(); $this->scanner->expectToken(','); $path = $this->parsePath(); } else { - $selector = null; + $selectorName = $this->implicitSelectorName; $path = $this->parsePath(); } $this->scanner->expectToken(')'); - return $this->factory->childNode($path, $selector); + return $this->factory->childNode($selectorName, $path); } /** @@ -545,16 +576,16 @@ protected function parseDescendantNode() { $this->scanner->expectTokens(array('ISDESCENDANTNODE', '(')); if ($this->scanner->tokenIs($this->scanner->lookupNextToken(1), ',')) { - $selector = $this->scanner->fetchNextToken(); + $selectorName = $this->scanner->fetchNextToken(); $this->scanner->expectToken(','); $path = $this->parsePath(); } else { - $selector = null; + $selectorName = $this->implicitSelectorName; $path = $this->parsePath(); } $this->scanner->expectToken(')'); - return $this->factory->descendantNode($path, $selector); + return $this->factory->descendantNode($selectorName, $path); } /** @@ -579,13 +610,13 @@ protected function parsePath() * 6.7.35 BindVariable * 6.7.36 Prefix * - * @return \PHPCR\Query\QOM\StaticOperandInterface + * @return StaticOperandInterface */ protected function parseStaticOperand() { $token = $this->scanner->lookupNextToken(); if (substr($token, 0, 1) === '$') { - return $this->factory->bindVariable(substr($token, 1)); + return $this->factory->bindVariable(substr($this->scanner->fetchNextToken(), 1)); } return $this->parseLiteral(); @@ -601,7 +632,7 @@ protected function parseStaticOperand() * 6.7.33 UpperCase * Parse an SQL2 dynamic operand * - * @return \PHPCR\Query\QOM\DynamicOperandInterface + * @return DynamicOperandInterface */ protected function parseDynamicOperand() { @@ -622,7 +653,7 @@ protected function parseDynamicOperand() $token = $this->scanner->fetchNextToken(); if ($this->scanner->tokenIs($token, ')')) { - return $this->factory->nodeName(); + return $this->factory->nodeName($this->implicitSelectorName); } $this->scanner->expectToken(')'); @@ -636,7 +667,7 @@ protected function parseDynamicOperand() $token = $this->scanner->fetchNextToken(); if ($this->scanner->tokenIs($token, ')')) { - return $this->factory->nodeLocalName(); + return $this->factory->nodeLocalName($this->implicitSelectorName); } $this->scanner->expectToken(')'); @@ -650,7 +681,7 @@ protected function parseDynamicOperand() $token = $this->scanner->fetchNextToken(); if ($this->scanner->tokenIs($token, ')')) { - return $this->factory->fullTextSearchScore(); + return $this->factory->fullTextSearchScore($this->implicitSelectorName); } $this->scanner->expectToken(')'); @@ -683,20 +714,20 @@ protected function parseDynamicOperand() * 6.7.27 PropertyValue * Parse an SQL2 property value * - * @return \PHPCR\Query\QOM\PropertyValueInterface + * @return PropertyValueInterface */ protected function parsePropertyValue() { - list($prop, $selector) = $this->parseIdentifier(); + list($selectorName, $prop) = $this->parseIdentifier(); - return $this->factory->propertyValue($prop, $selector); + return $this->factory->propertyValue($selectorName, $prop); } /** * 6.7.34 Literal * Parse an SQL2 literal value * - * @return \PHPCR\Query\QOM\LiteralInterface + * @return LiteralInterface */ protected function parseLiteral() { @@ -720,7 +751,7 @@ protected function parseLiteral() } if (substr($token, -1) !== $quoteString) { - throw new \Exception("Syntax error: unterminated quoted string $token in '{$this->sql2}'"); + throw new InvalidQueryException("Syntax error: unterminated quoted string $token in '{$this->sql2}'"); } $token = substr($token, 1, -1); $token = str_replace('\\'.$quoteString, $quoteString, $token); @@ -776,16 +807,18 @@ protected function parseOrdering() return $this->factory->ascending($operand); } - throw new \Exception("Syntax error: invalid ordering in '{$this->sql2}'"); + throw new InvalidQueryException("Syntax error: invalid ordering in '{$this->sql2}'"); } /** * 6.7.39 Column - * Parse an SQL2 columns definition and return an array of QOM\Column + * + * Scan the SQL2 columns definitions and return data arrays to convert to + * columns once the FROM is parsed. * * @return array of array */ - protected function parseColumns() + protected function scanColumns() { $this->scanner->expectToken('SELECT'); @@ -799,9 +832,8 @@ protected function parseColumns() $columns = array(); $hasNext = true; - // Column list while ($hasNext) { - $columns[] = $this->parseColumn(); + $columns[] = $this->scanColumn(); // Are there more columns? if ($this->scanner->lookupNextToken() !== ',') { @@ -815,6 +847,23 @@ protected function parseColumns() return $columns; } + /** + * Build the columns from the scanned column data. + * + * @param array $data + * + * @return ColumnInterface[] + */ + protected function buildColumns($data) + { + $columns = array(); + foreach ($data as $col) { + $columns[] = $this->buildColumn($col); + } + + return $columns; + } + /** * Get the next token and make sure to remove the brackets if the token is * in the [ns:name] notation @@ -836,9 +885,12 @@ private function fetchTokenWithoutBrackets() /** * Parse something that is expected to be a property identifier. * - * @return array with property name and selector name if specified, null otherwise + * @param boolean $checkSelector whether we need to ensure a valid selector. + * + * @return array with selectorName and propertyName. If no selectorName is + * specified, defaults to $this->defaultSelectorName */ - private function parseIdentifier() + private function parseIdentifier($checkSelector = true) { $token = $this->fetchTokenWithoutBrackets(); @@ -852,26 +904,92 @@ private function parseIdentifier() $propertyName = $token; } - return array($propertyName, $selectorName); + if ($checkSelector) { + $selectorName = $this->ensureSelectorName($selectorName); + } + + return array($selectorName, $propertyName); + } + + /** + * Add a selector name to the known selector names. + * + * @param string $selectorName + */ + protected function updateImplicitSelectorName($selectorName) + { + if (null === $this->implicitSelectorName) { + $this->implicitSelectorName = $selectorName; + } else { + if (!is_array($this->implicitSelectorName)) { + $this->implicitSelectorName = array($this->implicitSelectorName => $this->implicitSelectorName); + } + if (isset($this->implicitSelectorName[$selectorName])) { + throw new InvalidQueryException("Selector $selectorName is already in use"); + } + $this->implicitSelectorName[$selectorName] = $selectorName; + } + } + + /** + * Ensure that the parsedName is a valid selector, or return the implicit + * selector if its non-ambigous. + * + * @param string|null $parsedName + * + * @return string the selector to use + * + * @throws InvalidQueryException if there was no explicit selector and + * there is more than one selector available. + */ + protected function ensureSelectorName($parsedName) + { + if (null !== $parsedName) { + if (is_array($this->implicitSelectorName) && !isset($this->implicitSelectorName[$parsedName]) + || !is_array($this->implicitSelectorName) && $this->implicitSelectorName !== $parsedName + ) { + throw new InvalidQueryException("Unknown selector $parsedName in '{$this->sql2}'"); + } + + return $parsedName; + } + if (is_array($this->implicitSelectorName)) { + throw new InvalidQueryException("Need an explicit selector name in join queries"); + } + + return $this->implicitSelectorName; } /** - * Parse a single SQL2 column definition and return a QOM\Column + * Scan a single SQL2 column definition and return an array of information * - * @return \PHPCR\Query\QOM\ColumnInterface + * @return array */ - protected function parseColumn() + protected function scanColumn() { - list($propertyName, $selectorName) = $this->parseIdentifier(); + list($selectorName, $propertyName) = $this->parseIdentifier(false); // AS name if ($this->scanner->tokenIs($this->scanner->lookupNextToken(), 'AS')) { $this->scanner->fetchNextToken(); $columnName = $this->scanner->fetchNextToken(); } else { - $columnName = null; + $columnName = $propertyName; } - return $this->factory->column($propertyName, $columnName, $selectorName); + return array($selectorName, $propertyName, $columnName); + } + + /** + * Build a single SQL2 column definition + * + * @return ColumnInterface + */ + protected function buildColumn($data) + { + list($selectorName, $propertyName, $columnName) = $data; + $selectorName = $this->ensureSelectorName($selectorName); + + return $this->factory->column($selectorName, $propertyName, $columnName); } } diff --git a/tests/PHPCR/Tests/Util/QOM/QueryBuilderTest.php b/tests/PHPCR/Tests/Util/QOM/QueryBuilderTest.php index 0422312..56cc2c0 100644 --- a/tests/PHPCR/Tests/Util/QOM/QueryBuilderTest.php +++ b/tests/PHPCR/Tests/Util/QOM/QueryBuilderTest.php @@ -136,9 +136,9 @@ public function testSelect() { $qb = new QueryBuilder($this->qf); $this->assertEquals(0, count($qb->getColumns())); - $qb->select('propertyName', 'columnName', 'selectorName'); + $qb->select('selectorName', 'propertyName', 'columnName'); $this->assertEquals(1, count($qb->getColumns())); - $qb->select('propertyName', 'columnName', 'selectorName'); + $qb->select('selectorName', 'propertyName', 'columnName'); $this->assertEquals(1, count($qb->getColumns())); } @@ -146,9 +146,9 @@ public function testAddSelect() { $qb = new QueryBuilder($this->qf); $this->assertEquals(0, count($qb->getColumns())); - $qb->addSelect('propertyName', 'columnName', 'selectorName'); + $qb->addSelect('selectorName', 'propertyName', 'columnName'); $this->assertEquals(1, count($qb->getColumns())); - $qb->addSelect('propertyName', 'columnName', 'selectorName'); + $qb->addSelect('selectorName', 'propertyName', 'columnName'); $this->assertEquals(2, count($qb->getColumns())); } diff --git a/tests/PHPCR/Tests/Util/ValueConverterTest.php b/tests/PHPCR/Tests/Util/ValueConverterTest.php index 63aca84..b6ca3c1 100644 --- a/tests/PHPCR/Tests/Util/ValueConverterTest.php +++ b/tests/PHPCR/Tests/Util/ValueConverterTest.php @@ -272,7 +272,7 @@ public function dataConversionMatrix() */ public function testConvertType($value, $srctype, $expected, $targettype) { - if (is_null($expected)) { + if (null === $expected) { try { $this->valueConverter->convertType($value, $targettype, $srctype); $this->fail('Excpected that this conversion would throw an exception');