vendor/phpcr/phpcr-utils/src/PHPCR/Util/QOM/Sql2ToQomQueryConverter.php line 1051

Open in your IDE?
  1. <?php
  2. namespace PHPCR\Util\QOM;
  3. use DateTime;
  4. use Exception;
  5. use InvalidArgumentException;
  6. use LogicException;
  7. use PHPCR\PropertyType;
  8. use PHPCR\Query\InvalidQueryException;
  9. use PHPCR\Query\QOM\ChildNodeJoinConditionInterface;
  10. use PHPCR\Query\QOM\ColumnInterface;
  11. use PHPCR\Query\QOM\ComparisonInterface;
  12. use PHPCR\Query\QOM\ConstraintInterface;
  13. use PHPCR\Query\QOM\DescendantNodeJoinConditionInterface;
  14. use PHPCR\Query\QOM\DynamicOperandInterface;
  15. use PHPCR\Query\QOM\EquiJoinConditionInterface;
  16. use PHPCR\Query\QOM\FullTextSearchInterface;
  17. use PHPCR\Query\QOM\JoinConditionInterface;
  18. use PHPCR\Query\QOM\JoinInterface;
  19. use PHPCR\Query\QOM\NotInterface;
  20. use PHPCR\Query\QOM\OrderingInterface;
  21. use PHPCR\Query\QOM\PropertyValueInterface;
  22. use PHPCR\Query\QOM\QueryObjectModelConstantsInterface as Constants;
  23. use PHPCR\Query\QOM\QueryObjectModelFactoryInterface;
  24. use PHPCR\Query\QOM\QueryObjectModelInterface;
  25. use PHPCR\Query\QOM\SameNodeJoinConditionInterface;
  26. use PHPCR\Query\QOM\SelectorInterface;
  27. use PHPCR\Query\QOM\SourceInterface;
  28. use PHPCR\Query\QOM\StaticOperandInterface;
  29. use PHPCR\Util\ValueConverter;
  30. /**
  31.  * Parse SQL2 statements and output a corresponding QOM objects tree.
  32.  *
  33.  * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  34.  * @license http://opensource.org/licenses/MIT MIT License
  35.  */
  36. class Sql2ToQomQueryConverter
  37. {
  38.     /**
  39.      * The factory to create QOM objects.
  40.      *
  41.      * @var QueryObjectModelFactoryInterface
  42.      */
  43.     protected $factory;
  44.     /**
  45.      * Scanner to parse SQL2.
  46.      *
  47.      * @var Sql2Scanner;
  48.      */
  49.     protected $scanner;
  50.     /**
  51.      * The SQL2 query (the converter is not reentrant).
  52.      *
  53.      * @var string
  54.      */
  55.     protected $sql2;
  56.     /**
  57.      * The selector is not required for SQL2 but for QOM.
  58.      *
  59.      * We keep all selectors we encounter. If there is exactly one, it is used
  60.      * whenever we encounter non-qualified names.
  61.      *
  62.      * @var string|array
  63.      */
  64.     protected $implicitSelectorName null;
  65.     /**
  66.      * Instantiate a converter.
  67.      *
  68.      * @param QueryObjectModelFactoryInterface $factory
  69.      * @param ValueConverter                   $valueConverter To override default converter.
  70.      */
  71.     public function __construct(QueryObjectModelFactoryInterface $factoryValueConverter $valueConverter null)
  72.     {
  73.         $this->factory $factory;
  74.         $this->valueConverter $valueConverter ?: new ValueConverter();
  75.     }
  76.     /**
  77.      * 6.7.1. Query
  78.      * Parse an SQL2 query and return the corresponding QOM QueryObjectModel.
  79.      *
  80.      * @param string $sql2
  81.      *
  82.      * @throws InvalidQueryException
  83.      *
  84.      * @return QueryObjectModelInterface
  85.      */
  86.     public function parse($sql2)
  87.     {
  88.         $this->implicitSelectorName null;
  89.         $this->sql2 $sql2;
  90.         $this->scanner = new Sql2Scanner($sql2);
  91.         $source null;
  92.         $columnData = [];
  93.         $constraint null;
  94.         $orderings = [];
  95.         while ($this->scanner->lookupNextToken() !== '') {
  96.             switch (strtoupper($this->scanner->lookupNextToken())) {
  97.                 case 'SELECT':
  98.                     $this->scanner->expectToken('SELECT');
  99.                     $columnData $this->scanColumns();
  100.                     break;
  101.                 case 'FROM':
  102.                     $this->scanner->expectToken('FROM');
  103.                     $source $this->parseSource();
  104.                     break;
  105.                 case 'WHERE':
  106.                     $this->scanner->expectToken('WHERE');
  107.                     $constraint $this->parseConstraint();
  108.                     break;
  109.                 case 'ORDER':
  110.                     // Ordering, check there is a BY
  111.                     $this->scanner->expectTokens(['ORDER''BY']);
  112.                     $orderings $this->parseOrderings();
  113.                     break;
  114.                 default:
  115.                     throw new InvalidQueryException('Error parsing query, unknown query part "'.$this->scanner->lookupNextToken().'" in: '.$this->sql2);
  116.             }
  117.         }
  118.         if (!$source instanceof SourceInterface) {
  119.             throw new InvalidQueryException('Invalid query, source could not be determined: '.$sql2);
  120.         }
  121.         $columns $this->buildColumns($columnData);
  122.         return $this->factory->createQuery($source$constraint$orderings$columns);
  123.     }
  124.     /**
  125.      * 6.7.2. Source
  126.      * Parse an SQL2 source definition and return the corresponding QOM Source.
  127.      *
  128.      * @return SourceInterface
  129.      */
  130.     protected function parseSource()
  131.     {
  132.         $selector $this->parseSelector();
  133.         $next $this->scanner->lookupNextToken();
  134.         $left $selector;
  135.         while (in_array(strtoupper($next), ['JOIN''INNER''RIGHT''LEFT'])) {
  136.             $left $this->parseJoin($left);
  137.             $next $this->scanner->lookupNextToken();
  138.         }
  139.         return $left;
  140.     }
  141.     /**
  142.      * 6.7.3. Selector
  143.      * Parse an SQL2 selector and return a QOM\SelectorInterface.
  144.      *
  145.      * @return SelectorInterface
  146.      */
  147.     protected function parseSelector()
  148.     {
  149.         $nodetype $this->fetchTokenWithoutBrackets();
  150.         if ($this->scanner->tokenIs($this->scanner->lookupNextToken(), 'AS')) {
  151.             $this->scanner->fetchNextToken(); // Consume the AS
  152.             $selectorName $this->parseName();
  153.             $this->updateImplicitSelectorName($selectorName);
  154.             return $this->factory->selector($selectorName$nodetype);
  155.         }
  156.         $this->updateImplicitSelectorName($nodetype);
  157.         return $this->factory->selector($nodetype$nodetype);
  158.     }
  159.     /**
  160.      * 6.7.4. Name.
  161.      *
  162.      * @return string
  163.      */
  164.     protected function parseName()
  165.     {
  166.         return $this->scanner->fetchNextToken();
  167.     }
  168.     /**
  169.      * 6.7.5. Join
  170.      * 6.7.6. Join type
  171.      * Parse an SQL2 join source and return a QOM\Join.
  172.      *
  173.      * @param SourceInterface $leftSelector the left selector as it has been read by parseSource
  174.      *
  175.      * @return JoinInterface
  176.      */
  177.     protected function parseJoin(SourceInterface $leftSelector)
  178.     {
  179.         $joinType $this->parseJoinType();
  180.         $right $this->parseSelector();
  181.         $joinCondition $this->parseJoinCondition();
  182.         return $this->factory->join($leftSelector$right$joinType$joinCondition);
  183.     }
  184.     /**
  185.      * 6.7.6. Join type.
  186.      *
  187.      * @throws InvalidQueryException
  188.      *
  189.      * @return string
  190.      */
  191.     protected function parseJoinType()
  192.     {
  193.         $joinType Constants::JCR_JOIN_TYPE_INNER;
  194.         $token $this->scanner->fetchNextToken();
  195.         switch ($token) {
  196.             case 'JOIN':
  197.                 // Token already fetched, nothing to do
  198.                 break;
  199.             case 'INNER':
  200.                 $this->scanner->fetchNextToken();
  201.                 break;
  202.             case 'LEFT':
  203.                 $this->scanner->expectTokens(['OUTER''JOIN']);
  204.                 $joinType Constants::JCR_JOIN_TYPE_LEFT_OUTER;
  205.                 break;
  206.             case 'RIGHT':
  207.                 $this->scanner->expectTokens(['OUTER''JOIN']);
  208.                 $joinType Constants::JCR_JOIN_TYPE_RIGHT_OUTER;
  209.                 break;
  210.             default:
  211.                 throw new InvalidQueryException("Syntax error: Expected JOIN, INNER JOIN, RIGHT JOIN or LEFT JOIN in '{$this->sql2}'");
  212.         }
  213.         return $joinType;
  214.     }
  215.     /**
  216.      * 6.7.7. JoinCondition
  217.      * Parse an SQL2 join condition and return a JoinConditionInterface.
  218.      *
  219.      * @return JoinConditionInterface
  220.      */
  221.     protected function parseJoinCondition()
  222.     {
  223.         $this->scanner->expectToken('ON');
  224.         $token $this->scanner->lookupNextToken();
  225.         if ($this->scanner->tokenIs($token'ISSAMENODE')) {
  226.             return $this->parseSameNodeJoinCondition();
  227.         }
  228.         if ($this->scanner->tokenIs($token'ISCHILDNODE')) {
  229.             return $this->parseChildNodeJoinCondition();
  230.         }
  231.         if ($this->scanner->tokenIs($token'ISDESCENDANTNODE')) {
  232.             return $this->parseDescendantNodeJoinCondition();
  233.         }
  234.         return $this->parseEquiJoin();
  235.     }
  236.     /**
  237.      * 6.7.8. EquiJoinCondition
  238.      * Parse an SQL2 equijoin condition and return a EquiJoinConditionInterface.
  239.      *
  240.      * @return EquiJoinConditionInterface
  241.      */
  242.     protected function parseEquiJoin()
  243.     {
  244.         list($selectorName1$prop1) = $this->parseIdentifier();
  245.         $this->scanner->expectToken('=');
  246.         list($selectorName2$prop2) = $this->parseIdentifier();
  247.         return $this->factory->equiJoinCondition($selectorName1$prop1$selectorName2$prop2);
  248.     }
  249.     /**
  250.      * 6.7.9 SameNodeJoinCondition
  251.      * Parse an SQL2 same node join condition and return a SameNodeJoinConditionInterface.
  252.      *
  253.      * @return SameNodeJoinConditionInterface
  254.      */
  255.     protected function parseSameNodeJoinCondition()
  256.     {
  257.         $this->scanner->expectTokens(['ISSAMENODE''(']);
  258.         $selectorName1 $this->fetchTokenWithoutBrackets();
  259.         $this->scanner->expectToken(',');
  260.         $selectorName2 $this->fetchTokenWithoutBrackets();
  261.         $token $this->scanner->lookupNextToken();
  262.         if ($this->scanner->tokenIs($token',')) {
  263.             $this->scanner->fetchNextToken(); // consume the coma
  264.             $path $this->parsePath();
  265.         } else {
  266.             $path null;
  267.         }
  268.         $this->scanner->expectToken(')');
  269.         return $this->factory->sameNodeJoinCondition($selectorName1$selectorName2$path);
  270.     }
  271.     /**
  272.      * 6.7.10 ChildNodeJoinCondition
  273.      * Parse an SQL2 child node join condition and return a ChildNodeJoinConditionInterface.
  274.      *
  275.      * @return ChildNodeJoinConditionInterface
  276.      */
  277.     protected function parseChildNodeJoinCondition()
  278.     {
  279.         $this->scanner->expectTokens(['ISCHILDNODE''(']);
  280.         $child $this->fetchTokenWithoutBrackets();
  281.         $this->scanner->expectToken(',');
  282.         $parent $this->fetchTokenWithoutBrackets();
  283.         $this->scanner->expectToken(')');
  284.         return $this->factory->childNodeJoinCondition($child$parent);
  285.     }
  286.     /**
  287.      * 6.7.11 DescendantNodeJoinCondition
  288.      * Parse an SQL2 descendant node join condition and return a DescendantNodeJoinConditionInterface.
  289.      *
  290.      * @return DescendantNodeJoinConditionInterface
  291.      */
  292.     protected function parseDescendantNodeJoinCondition()
  293.     {
  294.         $this->scanner->expectTokens(['ISDESCENDANTNODE''(']);
  295.         $descendant $this->fetchTokenWithoutBrackets();
  296.         $this->scanner->expectToken(',');
  297.         $parent $this->fetchTokenWithoutBrackets();
  298.         $this->scanner->expectToken(')');
  299.         return $this->factory->descendantNodeJoinCondition($descendant$parent);
  300.     }
  301.     /**
  302.      * 6.7.13 And
  303.      * 6.7.14 Or.
  304.      *
  305.      * @param ConstraintInterface $lhs     Left hand side
  306.      * @param int                 $minprec Precedence
  307.      *
  308.      * @throws Exception
  309.      *
  310.      * @return ConstraintInterface
  311.      */
  312.     protected function parseConstraint($lhs null$minprec 0)
  313.     {
  314.         if ($lhs === null) {
  315.             $lhs $this->parsePrimaryConstraint();
  316.         }
  317.         $opprec = [
  318.             'OR'  => 1,
  319.             'AND' => 2,
  320.         ];
  321.         $op strtoupper($this->scanner->lookupNextToken());
  322.         while (isset($opprec[$op]) && $opprec[$op] >= $minprec) {
  323.             $this->scanner->fetchNextToken();
  324.             $rhs $this->parsePrimaryConstraint();
  325.             $nextop strtoupper($this->scanner->lookupNextToken());
  326.             while (isset($opprec[$nextop]) && $opprec[$nextop] > $opprec[$op]) {
  327.                 $rhs $this->parseConstraint($rhs$opprec[$nextop]);
  328.                 $nextop strtoupper($this->scanner->lookupNextToken());
  329.             }
  330.             switch ($op) {
  331.                 case 'AND':
  332.                     $lhs $this->factory->andConstraint($lhs$rhs);
  333.                     break;
  334.                 case 'OR':
  335.                     $lhs $this->factory->orConstraint($lhs$rhs);
  336.                     break;
  337.                 default:
  338.                     // this only happens if the operator is
  339.                     // in the $opprec-array but there is no
  340.                     // "elseif"-branch here for this operator.
  341.                     throw new Exception("Internal error: No action is defined for operator '$op'");
  342.             }
  343.             $op strtoupper($this->scanner->lookupNextToken());
  344.         }
  345.         return $lhs;
  346.     }
  347.     /**
  348.      * 6.7.12 Constraint.
  349.      *
  350.      * @return ConstraintInterface
  351.      */
  352.     protected function parsePrimaryConstraint()
  353.     {
  354.         $constraint null;
  355.         $token $this->scanner->lookupNextToken();
  356.         if ($this->scanner->tokenIs($token'NOT')) {
  357.             // NOT
  358.             $constraint $this->parseNot();
  359.         } elseif ($this->scanner->tokenIs($token'(')) {
  360.             // Grouping with parenthesis
  361.             $this->scanner->expectToken('(');
  362.             $constraint $this->parseConstraint();
  363.             $this->scanner->expectToken(')');
  364.         } elseif ($this->scanner->tokenIs($token'CONTAINS')) {
  365.             // Full Text Search
  366.             $constraint $this->parseFullTextSearch();
  367.         } elseif ($this->scanner->tokenIs($token'ISSAMENODE')) {
  368.             // SameNode
  369.             $constraint $this->parseSameNode();
  370.         } elseif ($this->scanner->tokenIs($token'ISCHILDNODE')) {
  371.             // ChildNode
  372.             $constraint $this->parseChildNode();
  373.         } elseif ($this->scanner->tokenIs($token'ISDESCENDANTNODE')) {
  374.             // DescendantNode
  375.             $constraint $this->parseDescendantNode();
  376.         } else {
  377.             // Is it a property existence?
  378.             $next1 $this->scanner->lookupNextToken(1);
  379.             if ($this->scanner->tokenIs($next1'IS')) {
  380.                 $constraint $this->parsePropertyExistence();
  381.             } elseif ($this->scanner->tokenIs($next1'.')) {
  382.                 $next2 $this->scanner->lookupNextToken(3);
  383.                 if ($this->scanner->tokenIs($next2'IS')) {
  384.                     $constraint $this->parsePropertyExistence();
  385.                 }
  386.             }
  387.             if ($constraint === null) {
  388.                 // It's not a property existence neither, then it's a comparison
  389.                 $constraint $this->parseComparison();
  390.             }
  391.         }
  392.         // No constraint read,
  393.         if ($constraint === null) {
  394.             throw new InvalidQueryException("Syntax error: constraint expected in '{$this->sql2}'");
  395.         }
  396.         return $constraint;
  397.     }
  398.     /**
  399.      * 6.7.15 Not.
  400.      *
  401.      * @return NotInterface
  402.      */
  403.     protected function parseNot()
  404.     {
  405.         $this->scanner->expectToken('NOT');
  406.         return $this->factory->notConstraint($this->parsePrimaryConstraint());
  407.     }
  408.     /**
  409.      * 6.7.16 Comparison.
  410.      *
  411.      * @throws InvalidQueryException
  412.      *
  413.      * @return ComparisonInterface
  414.      */
  415.     protected function parseComparison()
  416.     {
  417.         $op1 $this->parseDynamicOperand();
  418.         if (null === $op1) {
  419.             throw new InvalidQueryException("Syntax error: dynamic operator expected in '{$this->sql2}'");
  420.         }
  421.         $operator $this->parseOperator();
  422.         $op2 $this->parseStaticOperand();
  423.         return $this->factory->comparison($op1$operator$op2);
  424.     }
  425.     /**
  426.      * 6.7.17 Operator.
  427.      *
  428.      * @return string a constant from QueryObjectModelConstantsInterface
  429.      */
  430.     protected function parseOperator()
  431.     {
  432.         $token $this->scanner->fetchNextToken();
  433.         switch (strtoupper($token)) {
  434.             case '=':
  435.                 return Constants::JCR_OPERATOR_EQUAL_TO;
  436.             case '<>':
  437.                 return Constants::JCR_OPERATOR_NOT_EQUAL_TO;
  438.             case '<':
  439.                 return Constants::JCR_OPERATOR_LESS_THAN;
  440.             case '<=':
  441.                 return Constants::JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO;
  442.             case '>':
  443.                 return Constants::JCR_OPERATOR_GREATER_THAN;
  444.             case '>=':
  445.                 return Constants::JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO;
  446.             case 'LIKE':
  447.                 return Constants::JCR_OPERATOR_LIKE;
  448.         }
  449.         throw new InvalidQueryException("Syntax error: operator expected in '{$this->sql2}'");
  450.     }
  451.     /**
  452.      * 6.7.18 PropertyExistence.
  453.      *
  454.      * @return ConstraintInterface
  455.      */
  456.     protected function parsePropertyExistence()
  457.     {
  458.         list($selectorName$prop) = $this->parseIdentifier();
  459.         $this->scanner->expectToken('IS');
  460.         $token $this->scanner->lookupNextToken();
  461.         if ($this->scanner->tokenIs($token'NULL')) {
  462.             $this->scanner->fetchNextToken();
  463.             return $this->factory->notConstraint($this->factory->propertyExistence($selectorName$prop));
  464.         }
  465.         $this->scanner->expectTokens(['NOT''NULL']);
  466.         return $this->factory->propertyExistence($selectorName$prop);
  467.     }
  468.     /**
  469.      * 6.7.19 FullTextSearch.
  470.      *
  471.      * @return FullTextSearchInterface
  472.      */
  473.     protected function parseFullTextSearch()
  474.     {
  475.         $this->scanner->expectTokens(['CONTAINS''(']);
  476.         list($selectorName$propertyName) = $this->parseIdentifier();
  477.         $this->scanner->expectToken(',');
  478.         $expression $this->parseLiteralValue();
  479.         $this->scanner->expectToken(')');
  480.         return $this->factory->fullTextSearch($selectorName$propertyName$expression);
  481.     }
  482.     /**
  483.      * 6.7.20 SameNode.
  484.      */
  485.     protected function parseSameNode()
  486.     {
  487.         $this->scanner->expectTokens(['ISSAMENODE''(']);
  488.         if ($this->scanner->tokenIs($this->scanner->lookupNextToken(1), ',')) {
  489.             $selectorName $this->scanner->fetchNextToken();
  490.             $this->scanner->expectToken(',');
  491.             $path $this->parsePath();
  492.         } else {
  493.             $selectorName $this->implicitSelectorName;
  494.             $path $this->parsePath();
  495.         }
  496.         $this->scanner->expectToken(')');
  497.         return $this->factory->sameNode($selectorName$path);
  498.     }
  499.     /**
  500.      * 6.7.21 ChildNode.
  501.      */
  502.     protected function parseChildNode()
  503.     {
  504.         $this->scanner->expectTokens(['ISCHILDNODE''(']);
  505.         if ($this->scanner->tokenIs($this->scanner->lookupNextToken(1), ',')) {
  506.             $selectorName $this->scanner->fetchNextToken();
  507.             $this->scanner->expectToken(',');
  508.             $path $this->parsePath();
  509.         } else {
  510.             $selectorName $this->implicitSelectorName;
  511.             $path $this->parsePath();
  512.         }
  513.         $this->scanner->expectToken(')');
  514.         return $this->factory->childNode($selectorName$path);
  515.     }
  516.     /**
  517.      * 6.7.22 DescendantNode.
  518.      */
  519.     protected function parseDescendantNode()
  520.     {
  521.         $this->scanner->expectTokens(['ISDESCENDANTNODE''(']);
  522.         if ($this->scanner->tokenIs($this->scanner->lookupNextToken(1), ',')) {
  523.             $selectorName $this->scanner->fetchNextToken();
  524.             $this->scanner->expectToken(',');
  525.             $path $this->parsePath();
  526.         } else {
  527.             $selectorName $this->implicitSelectorName;
  528.             $path $this->parsePath();
  529.         }
  530.         $this->scanner->expectToken(')');
  531.         return $this->factory->descendantNode($selectorName$path);
  532.     }
  533.     /**
  534.      * Parse a JCR path consisting of either a simple path (a JCR name that contains
  535.      * only SQL-legal characters) or a path (simple path or quoted path) enclosed in
  536.      * square brackets. See JCR Spec Â§ 6.7.23.
  537.      *
  538.      * 6.7.23. Path
  539.      */
  540.     protected function parsePath()
  541.     {
  542.         $path $this->parseLiteralValue();
  543.         if (substr($path01) === '[' && substr($path, -1) === ']') {
  544.             $path substr($path1, -1);
  545.         }
  546.         return $path;
  547.     }
  548.     /**
  549.      * Parse an SQL2 static operand
  550.      * 6.7.35 BindVariable
  551.      * 6.7.36 Prefix.
  552.      *
  553.      * @return StaticOperandInterface
  554.      */
  555.     protected function parseStaticOperand()
  556.     {
  557.         $token $this->scanner->lookupNextToken();
  558.         if (substr($token01) === '$') {
  559.             return $this->factory->bindVariable(substr($this->scanner->fetchNextToken(), 1));
  560.         }
  561.         return $this->factory->literal($this->parseLiteralValue());
  562.     }
  563.     /**
  564.      * 6.7.26 DynamicOperand
  565.      * 6.7.28 Length
  566.      * 6.7.29 NodeName
  567.      * 6.7.30 NodeLocalName
  568.      * 6.7.31 FullTextSearchScore
  569.      * 6.7.32 LowerCase
  570.      * 6.7.33 UpperCase
  571.      * Parse an SQL2 dynamic operand.
  572.      *
  573.      * @return DynamicOperandInterface
  574.      */
  575.     protected function parseDynamicOperand()
  576.     {
  577.         $token $this->scanner->lookupNextToken();
  578.         if ($this->scanner->tokenIs($token'LENGTH')) {
  579.             $this->scanner->fetchNextToken();
  580.             $this->scanner->expectToken('(');
  581.             $val $this->parsePropertyValue();
  582.             $this->scanner->expectToken(')');
  583.             return $this->factory->length($val);
  584.         }
  585.         if ($this->scanner->tokenIs($token'NAME')) {
  586.             $this->scanner->fetchNextToken();
  587.             $this->scanner->expectToken('(');
  588.             $token $this->scanner->fetchNextToken();
  589.             if ($this->scanner->tokenIs($token')')) {
  590.                 return $this->factory->nodeName($this->implicitSelectorName);
  591.             }
  592.             $this->scanner->expectToken(')');
  593.             return $this->factory->nodeName($token);
  594.         }
  595.         if ($this->scanner->tokenIs($token'LOCALNAME')) {
  596.             $this->scanner->fetchNextToken();
  597.             $this->scanner->expectToken('(');
  598.             $token $this->scanner->fetchNextToken();
  599.             if ($this->scanner->tokenIs($token')')) {
  600.                 return $this->factory->nodeLocalName($this->implicitSelectorName);
  601.             }
  602.             $this->scanner->expectToken(')');
  603.             return $this->factory->nodeLocalName($token);
  604.         }
  605.         if ($this->scanner->tokenIs($token'SCORE')) {
  606.             $this->scanner->fetchNextToken();
  607.             $this->scanner->expectToken('(');
  608.             $token $this->scanner->fetchNextToken();
  609.             if ($this->scanner->tokenIs($token')')) {
  610.                 return $this->factory->fullTextSearchScore($this->implicitSelectorName);
  611.             }
  612.             $this->scanner->expectToken(')');
  613.             return $this->factory->fullTextSearchScore($token);
  614.         }
  615.         if ($this->scanner->tokenIs($token'LOWER')) {
  616.             $this->scanner->fetchNextToken();
  617.             $this->scanner->expectToken('(');
  618.             $op $this->parseDynamicOperand();
  619.             $this->scanner->expectToken(')');
  620.             return $this->factory->lowerCase($op);
  621.         }
  622.         if ($this->scanner->tokenIs($token'UPPER')) {
  623.             $this->scanner->fetchNextToken();
  624.             $this->scanner->expectToken('(');
  625.             $op $this->parseDynamicOperand();
  626.             $this->scanner->expectToken(')');
  627.             return $this->factory->upperCase($op);
  628.         }
  629.         return $this->parsePropertyValue();
  630.     }
  631.     /**
  632.      * 6.7.27 PropertyValue
  633.      * Parse an SQL2 property value.
  634.      *
  635.      * @return PropertyValueInterface
  636.      */
  637.     protected function parsePropertyValue()
  638.     {
  639.         list($selectorName$prop) = $this->parseIdentifier();
  640.         return $this->factory->propertyValue($selectorName$prop);
  641.     }
  642.     protected function parseCastLiteral($token)
  643.     {
  644.         if (!$this->scanner->tokenIs($token'CAST')) {
  645.             throw new LogicException('parseCastLiteral when not a CAST');
  646.         }
  647.         $this->scanner->expectToken('(');
  648.         $token $this->scanner->fetchNextToken();
  649.         $quoteString in_array($token[0], ['\'''"'], true);
  650.         if ($quoteString) {
  651.             $quotesUsed $token[0];
  652.             $token substr($token1, -1);
  653.             // Un-escaping quotes
  654.             $token str_replace('\\'.$quotesUsed$quotesUsed$token);
  655.         }
  656.         $this->scanner->expectToken('AS');
  657.         $type $this->scanner->fetchNextToken();
  658.         try {
  659.             $typeValue PropertyType::valueFromName($type);
  660.         } catch (InvalidArgumentException $e) {
  661.             throw new InvalidQueryException("Syntax error: attempting to cast to an invalid type '$type'");
  662.         }
  663.         $this->scanner->expectToken(')');
  664.         try {
  665.             $token $this->valueConverter->convertType($token$typeValuePropertyType::STRING);
  666.         } catch (Exception $e) {
  667.             throw new InvalidQueryException("Syntax error: attempting to cast string '$token' to type '$type'");
  668.         }
  669.         return $token;
  670.     }
  671.     /**
  672.      * 6.7.34 Literal
  673.      * Parse an SQL2 literal value.
  674.      *
  675.      * @return mixed
  676.      */
  677.     protected function parseLiteralValue()
  678.     {
  679.         $token $this->scanner->fetchNextToken();
  680.         if ($this->scanner->tokenIs($token'CAST')) {
  681.             return $this->parseCastLiteral($token);
  682.         }
  683.         $quoteString in_array($token[0], ['"'"'"], true);
  684.         if ($quoteString) {
  685.             $quotesUsed $token[0];
  686.             $token substr($token1, -1);
  687.             // Unescape quotes
  688.             $token str_replace('\\'.$quotesUsed$quotesUsed$token);
  689.             $token str_replace("''""'"$token);
  690.             if (preg_match('/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d+)?$/'$token)) {
  691.                 if (preg_match('/^\d{4}-\d{2}-\d{2}$/'$token)) {
  692.                     $token .= ' 00:00:00';
  693.                 }
  694.                 $token DateTime::createFromFormat('Y-m-d H:i:s'$token);
  695.             }
  696.         } elseif (is_numeric($token)) {
  697.             $token strpos($token'.') === false ? (int) $token : (float) $token;
  698.         } elseif ($token === 'true') {
  699.             $token true;
  700.         } elseif ($token === 'false') {
  701.             $token false;
  702.         }
  703.         return $token;
  704.     }
  705.     /**
  706.      * 6.7.37 Ordering.
  707.      */
  708.     protected function parseOrderings()
  709.     {
  710.         $orderings = [];
  711.         $continue true;
  712.         while ($continue) {
  713.             $orderings[] = $this->parseOrdering();
  714.             if ($this->scanner->tokenIs($this->scanner->lookupNextToken(), ',')) {
  715.                 $this->scanner->expectToken(',');
  716.             } else {
  717.                 $continue false;
  718.             }
  719.         }
  720.         return $orderings;
  721.     }
  722.     /**
  723.      * 6.7.38 Order.
  724.      *
  725.      * @return OrderingInterface
  726.      */
  727.     protected function parseOrdering()
  728.     {
  729.         $operand $this->parseDynamicOperand();
  730.         $token $this->scanner->lookupNextToken();
  731.         if ($this->scanner->tokenIs($token'DESC')) {
  732.             $this->scanner->expectToken('DESC');
  733.             return $this->factory->descending($operand);
  734.         }
  735.         if ($this->scanner->tokenIs($token'ASC') || ',' === $token || '' === $token) {
  736.             if ($this->scanner->tokenIs($token'ASC')) {
  737.                 $this->scanner->expectToken('ASC');
  738.             }
  739.             return $this->factory->ascending($operand);
  740.         }
  741.         throw new InvalidQueryException("Syntax error: invalid ordering in '{$this->sql2}'");
  742.     }
  743.     /**
  744.      * 6.7.39 Column.
  745.      *
  746.      * Scan the SQL2 columns definitions and return data arrays to convert to
  747.      * columns once the FROM is parsed.
  748.      *
  749.      * @return array of array
  750.      */
  751.     protected function scanColumns()
  752.     {
  753.         // Wildcard
  754.         if ($this->scanner->lookupNextToken() === '*') {
  755.             $this->scanner->fetchNextToken();
  756.             return [];
  757.         }
  758.         $columns = [];
  759.         $hasNext true;
  760.         while ($hasNext) {
  761.             $columns[] = $this->scanColumn();
  762.             // Are there more columns?
  763.             if ($this->scanner->lookupNextToken() !== ',') {
  764.                 $hasNext false;
  765.             } else {
  766.                 $this->scanner->fetchNextToken();
  767.             }
  768.         }
  769.         return $columns;
  770.     }
  771.     /**
  772.      * Build the columns from the scanned column data.
  773.      *
  774.      * @param array $data
  775.      *
  776.      * @return ColumnInterface[]
  777.      */
  778.     protected function buildColumns($data)
  779.     {
  780.         $columns = [];
  781.         foreach ($data as $col) {
  782.             $columns[] = $this->buildColumn($col);
  783.         }
  784.         return $columns;
  785.     }
  786.     /**
  787.      * Get the next token and make sure to remove the brackets if the token is
  788.      * in the [ns:name] notation.
  789.      *
  790.      * @return string
  791.      */
  792.     private function fetchTokenWithoutBrackets()
  793.     {
  794.         $token $this->scanner->fetchNextToken();
  795.         if (substr($token01) === '[' && substr($token, -1) === ']') {
  796.             // Remove brackets around the selector name
  797.             $token substr($token1, -1);
  798.         }
  799.         return $token;
  800.     }
  801.     /**
  802.      * Parse something that is expected to be a property identifier.
  803.      *
  804.      * @param bool $checkSelector whether we need to ensure a valid selector.
  805.      *
  806.      * @return array with selectorName and propertyName. If no selectorName is
  807.      *               specified, defaults to $this->defaultSelectorName
  808.      */
  809.     private function parseIdentifier($checkSelector true)
  810.     {
  811.         $token $this->fetchTokenWithoutBrackets();
  812.         // selector.property
  813.         if ($this->scanner->lookupNextToken() === '.') {
  814.             $selectorName $token;
  815.             $this->scanner->fetchNextToken();
  816.             $propertyName $this->fetchTokenWithoutBrackets();
  817.         } else {
  818.             $selectorName null;
  819.             $propertyName $token;
  820.         }
  821.         if ($checkSelector) {
  822.             $selectorName $this->ensureSelectorName($selectorName);
  823.         }
  824.         return [$selectorName$propertyName];
  825.     }
  826.     /**
  827.      * Add a selector name to the known selector names.
  828.      *
  829.      * @param string $selectorName
  830.      *
  831.      * @throws InvalidQueryException
  832.      */
  833.     protected function updateImplicitSelectorName($selectorName)
  834.     {
  835.         if (null === $this->implicitSelectorName) {
  836.             $this->implicitSelectorName $selectorName;
  837.         } else {
  838.             if (!is_array($this->implicitSelectorName)) {
  839.                 $this->implicitSelectorName = [$this->implicitSelectorName => $this->implicitSelectorName];
  840.             }
  841.             if (isset($this->implicitSelectorName[$selectorName])) {
  842.                 throw new InvalidQueryException("Selector $selectorName is already in use");
  843.             }
  844.             $this->implicitSelectorName[$selectorName] = $selectorName;
  845.         }
  846.     }
  847.     /**
  848.      * Ensure that the parsedName is a valid selector, or return the implicit
  849.      * selector if its non-ambigous.
  850.      *
  851.      * @param string|null $parsedName
  852.      *
  853.      * @throws InvalidQueryException if there was no explicit selector and
  854.      *                               there is more than one selector available.
  855.      *
  856.      * @return string the selector to use
  857.      */
  858.     protected function ensureSelectorName($parsedName)
  859.     {
  860.         if (null !== $parsedName) {
  861.             if (is_array($this->implicitSelectorName) && !isset($this->implicitSelectorName[$parsedName])
  862.                 || !is_array($this->implicitSelectorName) && $this->implicitSelectorName !== $parsedName
  863.             ) {
  864.                 throw new InvalidQueryException("Unknown selector $parsedName in '{$this->sql2}'");
  865.             }
  866.             return $parsedName;
  867.         }
  868.         if (is_array($this->implicitSelectorName)) {
  869.             throw new InvalidQueryException('Need an explicit selector name in join queries');
  870.         }
  871.         return $this->implicitSelectorName;
  872.     }
  873.     /**
  874.      * Scan a single SQL2 column definition and return an array of information.
  875.      *
  876.      * @return array
  877.      */
  878.     protected function scanColumn()
  879.     {
  880.         list($selectorName$propertyName) = $this->parseIdentifier(false);
  881.         // AS name
  882.         if ($this->scanner->tokenIs($this->scanner->lookupNextToken(), 'AS')) {
  883.             $this->scanner->fetchNextToken();
  884.             $columnName $this->scanner->fetchNextToken();
  885.         } else {
  886.             $columnName $propertyName;
  887.         }
  888.         return [$selectorName$propertyName$columnName];
  889.     }
  890.     /**
  891.      * Build a single SQL2 column definition.
  892.      *
  893.      * @param array $data with selector name, property name and column name.
  894.      *
  895.      * @return ColumnInterface
  896.      */
  897.     protected function buildColumn(array $data)
  898.     {
  899.         list($selectorName$propertyName$columnName) = $data;
  900.         $selectorName $this->ensureSelectorName($selectorName);
  901.         return $this->factory->column($selectorName$propertyName$columnName);
  902.     }
  903. }