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

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 $factory, ValueConverter $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($path, 0, 1) === '[' && substr($path, -1) === ']') {
  548. $path = substr($path, 1, -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($token, 0, 1) === '$') {
  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($token, 1, -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, $typeValue, PropertyType::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($token, 1, -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($token, 0, 1) === '[' && substr($token, -1) === ']') {
  800. // Remove brackets around the selector name
  801. $token = substr($token, 1, -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. }