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

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