vendor/jackalope/jackalope-doctrine-dbal/src/Jackalope/Transport/DoctrineDBAL/Query/QOMWalker.php line 137

Open in your IDE?
  1. <?php
  2. namespace Jackalope\Transport\DoctrineDBAL\Query;
  3. use BadMethodCallException;
  4. use DateTime;
  5. use DateTimeZone;
  6. use Doctrine\DBAL\Platforms\MySQLPlatform;
  7. use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
  8. use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
  9. use Doctrine\DBAL\Schema\Schema;
  10. use Jackalope\NotImplementedException;
  11. use Jackalope\Query\QOM\PropertyValue;
  12. use Jackalope\Query\QOM\QueryObjectModel;
  13. use Jackalope\Transport\DoctrineDBAL\RepositorySchema;
  14. use Jackalope\Transport\DoctrineDBAL\Util\Xpath;
  15. use PHPCR\NamespaceException;
  16. use PHPCR\NodeType\NodeTypeInterface;
  17. use PHPCR\NodeType\NodeTypeManagerInterface;
  18. use PHPCR\Query\InvalidQueryException;
  19. use PHPCR\Query\QOM;
  20. use Doctrine\DBAL\Connection;
  21. use Doctrine\DBAL\Platforms\AbstractPlatform;
  22. use Doctrine\DBAL\Platforms\SqlitePlatform;
  23. /**
  24.  * Converts QOM to SQL Statements for the Doctrine DBAL database backend.
  25.  *
  26.  * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  27.  * @license http://opensource.org/licenses/MIT MIT License
  28.  */
  29. class QOMWalker
  30. {
  31.     /**
  32.      * @var NodeTypeManagerInterface
  33.      */
  34.     private $nodeTypeManager;
  35.     /**
  36.      * @var array
  37.      */
  38.     private $alias = [];
  39.     /**
  40.      * @var QOM\SelectorInterface
  41.      */
  42.     private $source;
  43.     /**
  44.      * @var Connection
  45.      */
  46.     private $conn;
  47.     /**
  48.      * @var AbstractPlatform
  49.      */
  50.     private $platform;
  51.     /**
  52.      * @var array
  53.      */
  54.     private $namespaces;
  55.     /**
  56.      * @var Schema
  57.      */
  58.     private $schema;
  59.     /**
  60.      * @param NodeTypeManagerInterface $manager
  61.      * @param Connection               $conn
  62.      * @param array                    $namespaces
  63.      */
  64.     public function __construct(NodeTypeManagerInterface $managerConnection $conn, array $namespaces = [])
  65.     {
  66.         $this->conn $conn;
  67.         $this->nodeTypeManager $manager;
  68.         $this->platform $conn->getDatabasePlatform();
  69.         $this->namespaces $namespaces;
  70.         $this->schema = new RepositorySchema([], $this->conn);
  71.     }
  72.     /**
  73.      * Generate a table alias
  74.      *
  75.      * @param string $selectorName
  76.      *
  77.      * @return string
  78.      */
  79.     private function getTableAlias($selectorName)
  80.     {
  81.         $selectorAlias $this->getSelectorAlias($selectorName);
  82.         if (!isset($this->alias[$selectorAlias])) {
  83.             $this->alias[$selectorAlias] = 'n' count($this->alias);
  84.         }
  85.         return $this->alias[$selectorAlias];
  86.     }
  87.     /**
  88.      * @param string $selectorName
  89.      *
  90.      * @return string
  91.      */
  92.     private function getSelectorAlias($selectorName)
  93.     {
  94.         if (null === $selectorName) {
  95.             if (count($this->alias)) { // We have aliases, use the first
  96.                 $selectorAlias array_search('n0'$this->alias);
  97.             } else { // Currently no aliases, use an empty string as index
  98.                 $selectorAlias '';
  99.             }
  100.         } elseif (strpos($selectorName'.') === false) {
  101.             $selectorAlias $selectorName;
  102.         } else {
  103.             $parts explode('.'$selectorName);
  104.             $selectorAlias reset($parts);
  105.         }
  106.         if (strpos($selectorAlias'[') === 0) {
  107.             $selectorAlias substr($selectorAlias1, -1);
  108.         }
  109.         if ($this->source && $this->source->getNodeTypeName() === $selectorAlias) {
  110.             $selectorAlias '';
  111.         }
  112.         return $selectorAlias;
  113.     }
  114.     /**
  115.      * @param QueryObjectModel $qom
  116.      *
  117.      * @return string
  118.      */
  119.     public function walkQOMQuery(QueryObjectModel $qom)
  120.     {
  121.         $source $qom->getSource();
  122.         $selectors $this->validateSource($source);
  123.         $sourceSql ' ' $this->walkSource($source);
  124.         $constraintSql '';
  125.         if ($constraint $qom->getConstraint()) {
  126.             $constraintSql ' AND ' $this->walkConstraint($constraint);
  127.         }
  128.         $orderingSql '';
  129.         if ($orderings $qom->getOrderings()) {
  130.             $orderingSql ' ' $this->walkOrderings($orderings);
  131.         }
  132.         $sql 'SELECT ' $this->getColumns($qom);
  133.         $sql .= $sourceSql;
  134.         $sql .= $constraintSql;
  135.         $sql .= $orderingSql;
  136.         $limit $qom->getLimit();
  137.         $offset $qom->getOffset();
  138.         if (null !== $offset && null === $limit
  139.             && ($this->platform instanceof MySQLPlatform || $this->platform instanceof SqlitePlatform)
  140.         ) {
  141.             $limit PHP_INT_MAX;
  142.         }
  143.         $sql $this->platform->modifyLimitQuery($sql$limit$offset);
  144.         return [$selectors$this->alias$sql];
  145.     }
  146.     /**
  147.      * @return string
  148.      */
  149.     public function getColumns(QueryObjectModel $qom)
  150.     {
  151.         // TODO we should actually build Xpath statements for each column we actually need in the result and not fetch all 'props'
  152.         $sqlColumns = ['path''identifier''props'];
  153.         if (count($this->alias)) {
  154.             $aliasSql = [];
  155.             foreach ($this->alias as $alias) {
  156.                 foreach ($sqlColumns as $sqlColumn) {
  157.                     $aliasSql[] = sprintf('%s.%s AS %s_%s'$alias$sqlColumn$alias$sqlColumn);
  158.                 }
  159.             }
  160.             return implode(', '$aliasSql);
  161.         }
  162.         return '*';
  163.     }
  164.     /**
  165.      * Validates the nodeTypes in given source
  166.      *
  167.      * @param QOM\SourceInterface $source
  168.      *
  169.      * @return QOM\SelectorInterface[]
  170.      *
  171.      * @throws InvalidQueryException
  172.      */
  173.     protected function validateSource(QOM\SourceInterface $source)
  174.     {
  175.         if ($source instanceof QOM\SelectorInterface) {
  176.             $selectors = [$source];
  177.             $this->validateSelectorSource($source);
  178.         } elseif ($source instanceof QOM\JoinInterface) {
  179.             $selectors $this->validateJoinSource($source);
  180.         } else {
  181.             $selectors = [];
  182.         }
  183.         return $selectors;
  184.     }
  185.     /**
  186.      * @param QOM\SelectorInterface $source
  187.      *
  188.      * @throws InvalidQueryException
  189.      */
  190.     protected function validateSelectorSource(QOM\SelectorInterface $source)
  191.     {
  192.         $nodeType $source->getNodeTypeName();
  193.         if (!$this->nodeTypeManager->hasNodeType($nodeType)) {
  194.             $msg 'Selected node type does not exist: ' $nodeType;
  195.             if ($alias $source->getSelectorName()) {
  196.                 $msg .= ' AS ' $alias;
  197.             }
  198.             throw new InvalidQueryException($msg);
  199.         }
  200.     }
  201.     /**
  202.      * @param QOM\JoinInterface $source
  203.      *
  204.      * @return QOM\SelectorInterface[]
  205.      *
  206.      * @throws InvalidQueryException
  207.      */
  208.     protected function validateJoinSource(QOM\JoinInterface $source)
  209.     {
  210.         $left $source->getLeft();
  211.         $right $source->getRight();
  212.         if ($left) {
  213.             $selectors $this->validateSource($left);
  214.         } else {
  215.             $selectors = [];
  216.         }
  217.         if ($right) {
  218.             // Ensure that the primary selector is first
  219.             if (QOM\QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_RIGHT_OUTER === $source->getJoinType()) {
  220.                 $selectors array_merge($this->validateSource($right), $selectors);
  221.             } else {
  222.                 $selectors array_merge($selectors$this->validateSource($right));
  223.             }
  224.         }
  225.         return $selectors;
  226.     }
  227.     /**
  228.      * @param QOM\SourceInterface $source
  229.      *
  230.      * @return string
  231.      *
  232.      * @throws NotImplementedException
  233.      */
  234.     public function walkSource(QOM\SourceInterface $source)
  235.     {
  236.         if ($source instanceof QOM\SelectorInterface) {
  237.             return $this->walkSelectorSource($source);
  238.         }
  239.         if ($source instanceof QOM\JoinInterface) {
  240.             return $this->walkJoinSource($source);
  241.         }
  242.         throw new NotImplementedException(sprintf("The source class '%s' is not supported"get_class($source)));
  243.     }
  244.     /**
  245.      * @param QOM\SelectorInterface $source
  246.      *
  247.      * @return string
  248.      */
  249.     public function walkSelectorSource(QOM\SelectorInterface $source)
  250.     {
  251.         $this->source $source;
  252.         $alias $this->getTableAlias($source->getSelectorName());
  253.         $nodeTypeClause $this->sqlNodeTypeClause($alias$source);
  254.         $sql "FROM phpcr_nodes $alias WHERE $alias.workspace_name = ? AND $nodeTypeClause";
  255.         return $sql;
  256.     }
  257.     /**
  258.      * @param QOM\JoinConditionInterface $right
  259.      *
  260.      * @return string the alias on the right side of a join
  261.      *
  262.      * @throws BadMethodCallException if the provided JoinCondition has no valid way of getting the right selector
  263.      */
  264.     private function getRightJoinSelector(QOM\JoinConditionInterface $right)
  265.     {
  266.         if ($right instanceof QOM\ChildNodeJoinConditionInterface) {
  267.             return $right->getParentSelectorName();
  268.         } elseif ($right instanceof QOM\DescendantNodeJoinConditionInterface) {
  269.             return $right->getAncestorSelectorName();
  270.         } elseif ($right instanceof QOM\SameNodeJoinConditionInterface || $right instanceof QOM\EquiJoinConditionInterface) {
  271.             return $right->getSelector2Name();
  272.         }
  273.         throw new BadMethodCallException('Supplied join type should implement getSelector2Name() or be an instance of ChildNodeJoinConditionInterface or DescendantNodeJoinConditionInterface');
  274.     }
  275.     /**
  276.      * @param QOM\JoinConditionInterface $right
  277.      *
  278.      * @return string the alias on the left side of a join
  279.      *
  280.      * @throws BadMethodCallException if the provided JoinCondition has no valid way of getting the left selector
  281.      */
  282.     private function getLeftJoinSelector(QOM\JoinConditionInterface $left)
  283.     {
  284.         if ($left instanceof QOM\ChildNodeJoinConditionInterface) {
  285.             return $left->getChildSelectorName();
  286.         } elseif ($left instanceof QOM\DescendantNodeJoinConditionInterface) {
  287.             return $left->getAncestorSelectorName();
  288.         } elseif ($left instanceof QOM\SameNodeJoinConditionInterface || $left instanceof QOM\EquiJoinConditionInterface) {
  289.             return $left->getSelector1Name();
  290.         }
  291.         throw new BadMethodCallException('Supplied join type should implement getSelector2Name() or be an instance of ChildNodeJoinConditionInterface or DescendantNodeJoinConditionInterface');
  292.     }
  293.     /**
  294.      * find the most left join in a tree
  295.      *
  296.      * @param QOM\JoinInterface $source
  297.      *
  298.      * @return QOM\JoinInterface
  299.      */
  300.     private function getLeftMostJoin(QOM\JoinInterface $source)
  301.     {
  302.         if ($source->getLeft() instanceof QOM\JoinInterface) {
  303.             return $this->getLeftMostJoin($source->getLeft());
  304.         }
  305.         return $source;
  306.     }
  307.     /**
  308.      * @param QOM\JoinInterface $source
  309.      * @param boolean $root whether the method call is recursed for nested joins. If true, it will add a WHERE clause
  310.      *        that checks the workspace_name and type
  311.      *
  312.      * @return string
  313.      *
  314.      * @throws NotImplementedException if the right side of the join consists of another join
  315.      */
  316.     public function walkJoinSource(QOM\JoinInterface $source$root true)
  317.     {
  318.         $this->source $left $source->getLeft(); // The $left variable is used for storing the leftmost selector
  319.         if (!$source->getRight() instanceof QOM\SelectorInterface) {
  320.             throw new NotImplementedException('The right side of the join should not consist of another join');
  321.         }
  322.         if ($source->getLeft() instanceof QOM\SelectorInterface) {
  323.             $leftAlias $this->getTableAlias($source->getLeft()->getSelectorName());
  324.             $this->getTableAlias($source->getLeft()->getSelectorName());
  325.             $sql "FROM phpcr_nodes $leftAlias ";
  326.         } else {
  327.             $sql $this->walkJoinSource($leftfalse) . ' '// One step left, until we're at the selector
  328.             $leftMostJoin $this->getLeftMostJoin($source);
  329.             $leftAlias $this->getTableAlias(
  330.                 $this->getLeftJoinSelector($leftMostJoin->getJoinCondition())
  331.             );
  332.             $left $leftMostJoin->getLeft();
  333.         }
  334.         $rightAlias $this->getTableAlias($source->getRight()->getSelectorName());
  335.         $nodeTypeClause $this->sqlNodeTypeClause($rightAlias$source->getRight());
  336.         switch ($source->getJoinType()) {
  337.             case QOM\QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_INNER:
  338.                 $sql .= sprintf("INNER JOIN phpcr_nodes %s "$rightAlias);
  339.                 break;
  340.             case QOM\QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_LEFT_OUTER:
  341.                 $sql .= sprintf("LEFT JOIN phpcr_nodes %s "$rightAlias);
  342.                 break;
  343.             case QOM\QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_RIGHT_OUTER:
  344.                 $sql .= sprintf("RIGHT JOIN phpcr_nodes %s "$rightAlias);
  345.                 break;
  346.         }
  347.         $sql .= sprintf("ON ( %s.workspace_name = %s.workspace_name AND %s "$leftAlias$rightAlias$nodeTypeClause);
  348.         $sql .= 'AND ' $this->walkJoinCondition($source->getLeft(), $source->getRight(), $source->getJoinCondition()) . ' ';
  349.         $sql .= ') '// close on-clause
  350.         if ($root) { // The method call is not recursed when $root is true, so we can add a WHERE clause
  351.             // TODO: revise this part for alternatives
  352.             $sql .= sprintf("WHERE %s.workspace_name = ? AND %s.type IN ('%s'"$leftAlias$leftAlias$left->getNodeTypeName());
  353.             $subTypes $this->nodeTypeManager->getSubtypes($left->getNodeTypeName());
  354.             foreach ($subTypes as $subType) {
  355.                 /* @var $subType NodeTypeInterface */
  356.                 $sql .= sprintf(", '%s'"$subType->getName());
  357.             }
  358.             $sql .= ')';
  359.         }
  360.         return $sql;
  361.     }
  362.     /**
  363.      * @param QOM\SelectorInterface|QOM\JoinInterface $left
  364.      * @param QOM\SelectorInterface $right
  365.      * @param QOM\JoinConditionInterface $condition
  366.      *
  367.      * @return string
  368.      *
  369.      * @throws NotImplementedException if a SameNodeJoinCondtion is used.
  370.      */
  371.     public function walkJoinCondition($leftQOM\SelectorInterface $rightQOM\JoinConditionInterface $condition)
  372.     {
  373.         if ($condition instanceof QOM\ChildNodeJoinConditionInterface) {
  374.             return $this->walkChildNodeJoinCondition($condition);
  375.         }
  376.         if ($condition instanceof QOM\DescendantNodeJoinConditionInterface) {
  377.             return $this->walkDescendantNodeJoinCondition($condition);
  378.         }
  379.         if ($condition instanceof QOM\EquiJoinConditionInterface) {
  380.             if ($left instanceof QOM\SelectorInterface) {
  381.                 $selectorName $left->getSelectorName();
  382.             } else {
  383.                 $selectorName $this->getLeftJoinSelector($this->getLeftMostJoin($left)->getJoinCondition());
  384.             }
  385.             return $this->walkEquiJoinCondition($selectorName$right->getSelectorName(), $condition);
  386.         }
  387.         if ($condition instanceof QOM\SameNodeJoinConditionInterface) {
  388.             throw new NotImplementedException('SameNodeJoinCondtion');
  389.         }
  390.     }
  391.     /**
  392.      * @param QOM\ChildNodeJoinConditionInterface $condition
  393.      *
  394.      * @return string
  395.      */
  396.     public function walkChildNodeJoinCondition(QOM\ChildNodeJoinConditionInterface $condition)
  397.     {
  398.         $rightAlias $this->getTableAlias($condition->getChildSelectorName());
  399.         $leftAlias $this->getTableAlias($condition->getParentSelectorName());
  400.         $concatExpression $this->platform->getConcatExpression("$leftAlias.path""'/%'");
  401.         return sprintf("(%s.path LIKE %s AND %s.depth = %s.depth + 1) "$rightAlias$concatExpression$rightAlias$leftAlias);
  402.     }
  403.     /**
  404.      * @param QOM\DescendantNodeJoinConditionInterface $condition
  405.      *
  406.      * @return string
  407.      */
  408.     public function walkDescendantNodeJoinCondition(QOM\DescendantNodeJoinConditionInterface $condition)
  409.     {
  410.         $rightAlias $this->getTableAlias($condition->getDescendantSelectorName());
  411.         $leftAlias $this->getTableAlias($condition->getAncestorSelectorName());
  412.         $concatExpression $this->platform->getConcatExpression("$leftAlias.path""'/%'");
  413.         return sprintf("%s.path LIKE %s "$rightAlias$concatExpression);
  414.     }
  415.     /**
  416.      * @param QOM\EquiJoinConditionInterface $condition
  417.      *
  418.      * @return string
  419.      */
  420.     public function walkEquiJoinCondition($leftSelectorName$rightSelectorNameQOM\EquiJoinConditionInterface $condition)
  421.     {
  422.         return $this->walkOperand(new PropertyValue($leftSelectorName$condition->getProperty1Name())) . ' ' .
  423.                $this->walkOperator(QOM\QueryObjectModelConstantsInterface::JCR_OPERATOR_EQUAL_TO) . ' ' .
  424.                $this->walkOperand(new PropertyValue($rightSelectorName$condition->getProperty2Name()));
  425.     }
  426.     /**
  427.      * @param \PHPCR\Query\QOM\ConstraintInterface $constraint
  428.      *
  429.      * @return string
  430.      *
  431.      * @throws InvalidQueryException
  432.      */
  433.     public function walkConstraint(QOM\ConstraintInterface $constraint)
  434.     {
  435.         if ($constraint instanceof QOM\AndInterface) {
  436.             return $this->walkAndConstraint($constraint);
  437.         }
  438.         if ($constraint instanceof QOM\OrInterface) {
  439.             return $this->walkOrConstraint($constraint);
  440.         }
  441.         if ($constraint instanceof QOM\NotInterface) {
  442.             return $this->walkNotConstraint($constraint);
  443.         }
  444.         if ($constraint instanceof QOM\ComparisonInterface) {
  445.             return $this->walkComparisonConstraint($constraint);
  446.         }
  447.         if ($constraint instanceof QOM\DescendantNodeInterface) {
  448.             return $this->walkDescendantNodeConstraint($constraint);
  449.         }
  450.         if ($constraint instanceof QOM\ChildNodeInterface) {
  451.             return $this->walkChildNodeConstraint($constraint);
  452.         }
  453.         if ($constraint instanceof QOM\PropertyExistenceInterface) {
  454.             return $this->walkPropertyExistenceConstraint($constraint);
  455.         }
  456.         if ($constraint instanceof QOM\SameNodeInterface) {
  457.             return $this->walkSameNodeConstraint($constraint);
  458.         }
  459.         if ($constraint instanceof QOM\FullTextSearchInterface) {
  460.             return $this->walkFullTextSearchConstraint($constraint);
  461.         }
  462.         throw new InvalidQueryException(sprintf("Constraint %s not yet supported."get_class($constraint)));
  463.     }
  464.     /**
  465.      * @param QOM\SameNodeInterface $constraint
  466.      *
  467.      * @return string
  468.      */
  469.     public function walkSameNodeConstraint(QOM\SameNodeInterface $constraint)
  470.     {
  471.         return sprintf(
  472.             "%s.path = '%s'",
  473.             $this->getTableAlias($constraint->getSelectorName()),
  474.             $constraint->getPath()
  475.         );
  476.     }
  477.     /**
  478.      * @param QOM\FullTextSearchInterface $constraint
  479.      *
  480.      * @return string
  481.      */
  482.     public function walkFullTextSearchConstraint(QOM\FullTextSearchInterface $constraint)
  483.     {
  484.         return sprintf('%s LIKE %s',
  485.             $this->sqlXpathExtractValue($this->getTableAlias($constraint->getSelectorName()), $constraint->getPropertyName()),
  486.             $this->conn->quote('%'.$constraint->getFullTextSearchExpression().'%')
  487.         );
  488.     }
  489.     /**
  490.      * @param QOM\PropertyExistenceInterface $constraint
  491.      *
  492.      * @return string
  493.      */
  494.     public function walkPropertyExistenceConstraint(QOM\PropertyExistenceInterface $constraint)
  495.     {
  496.         return $this->sqlXpathValueExists($this->getTableAlias($constraint->getSelectorName()), $constraint->getPropertyName());
  497.     }
  498.     /**
  499.      * @param QOM\DescendantNodeInterface $constraint
  500.      *
  501.      * @return string
  502.      */
  503.     public function walkDescendantNodeConstraint(QOM\DescendantNodeInterface $constraint)
  504.     {
  505.         $ancestorPath $constraint->getAncestorPath();
  506.         if ('/' === $ancestorPath) {
  507.             $ancestorPath '';
  508.         } elseif (substr($ancestorPath, -1) === '/') {
  509.             throw new InvalidQueryException("Trailing slash in $ancestorPath");
  510.         }
  511.         return sprintf(
  512.             "%s.path LIKE '%s/%%'",
  513.             $this->getTableAlias($constraint->getSelectorName()),
  514.             addcslashes($ancestorPath"'")
  515.         );
  516.     }
  517.     /**
  518.      * @param QOM\ChildNodeInterface $constraint
  519.      *
  520.      * @return string
  521.      */
  522.     public function walkChildNodeConstraint(QOM\ChildNodeInterface $constraint)
  523.     {
  524.         return sprintf(
  525.             "%s.parent = '%s'",
  526.             $this->getTableAlias($constraint->getSelectorName()),
  527.             addcslashes($constraint->getParentPath(), "'")
  528.         );
  529.     }
  530.     /**
  531.      * @param QOM\AndInterface $constraint
  532.      *
  533.      * @return string
  534.      */
  535.     public function walkAndConstraint(QOM\AndInterface $constraint)
  536.     {
  537.         return sprintf(
  538.             "(%s AND %s)",
  539.             $this->walkConstraint($constraint->getConstraint1()),
  540.             $this->walkConstraint($constraint->getConstraint2())
  541.         );
  542.     }
  543.     /**
  544.      * @param QOM\OrInterface $constraint
  545.      *
  546.      * @return string
  547.      */
  548.     public function walkOrConstraint(QOM\OrInterface $constraint)
  549.     {
  550.         return sprintf(
  551.             "(%s OR %s)",
  552.             $this->walkConstraint($constraint->getConstraint1()),
  553.             $this->walkConstraint($constraint->getConstraint2())
  554.         );
  555.     }
  556.     /**
  557.      * @param QOM\NotInterface $constraint
  558.      *
  559.      * @return string
  560.      */
  561.     public function walkNotConstraint(QOM\NotInterface $constraint)
  562.     {
  563.         return sprintf(
  564.             "NOT (%s)",
  565.             $this->walkConstraint($constraint->getConstraint())
  566.         );
  567.     }
  568.     /**
  569.      * This method figures out the best way to do a comparison
  570.      * When we need to compare a property with a literal value,
  571.      * we need to be aware of the multivalued properties, we then require
  572.      * a different xpath statement then with other comparisons
  573.      *
  574.      * @param QOM\ComparisonInterface $constraint
  575.      *
  576.      * @return string
  577.      */
  578.     public function walkComparisonConstraint(QOM\ComparisonInterface $constraint)
  579.     {
  580.         $operator $this->walkOperator($constraint->getOperator());
  581.         $operator1 $constraint->getOperand1();
  582.         $operator2 $constraint->getOperand2();
  583.         // Check if we have a property and a literal value (in random order)
  584.         if (
  585.             ($operator1 instanceof QOM\PropertyValueInterface
  586.                 && $operator2 instanceof QOM\LiteralInterface)
  587.             || ($operator1 instanceof QOM\LiteralInterface
  588.                 && $operator2 instanceof QOM\PropertyValueInterface)
  589.             || ($operator1 instanceof QOM\NodeNameInterface
  590.                 && $operator2 instanceof QOM\LiteralInterface)
  591.             || ($operator1 instanceof QOM\LiteralInterface
  592.                 && $operator2 instanceof QOM\NodeNameInterface)
  593.         ) {
  594.             // Check whether the left is the literal, at this point the other always is the literal/nodename operand
  595.             if ($operator1 instanceof QOM\LiteralInterface) {
  596.                 $operand $operator2;
  597.                 $literalOperand $operator1;
  598.             } else {
  599.                 $literalOperand $operator2;
  600.                 $operand $operator1;
  601.             }
  602.             if (is_string($literalOperand->getLiteralValue()) && '=' !== $operator && '!=' !== $operator) {
  603.                 return
  604.                     $this->walkOperand($operator1) . ' ' .
  605.                     $operator ' ' .
  606.                     $this->walkOperand($operator2);
  607.             }
  608.             if ($operand instanceof QOM\NodeNameInterface) {
  609.                 $selectorName $operand->getSelectorName();
  610.                 $alias $this->getTableAlias($selectorName);
  611.                 $literal $literalOperand->getLiteralValue();
  612.                 if (false !== strpos($literal':')) {
  613.                     $parts explode(':'$literal);
  614.                     if (!isset($this->namespaces[$parts[0]])) {
  615.                         throw new NamespaceException('The namespace ' $parts[0] . ' was not registered.');
  616.                     }
  617.                     $parts[0] = $this->namespaces[$parts[0]];
  618.                     $literal implode(':'$parts);
  619.                 }
  620.                 return sprintf(
  621.                     '%s %s %s',
  622.                     $this->platform->getConcatExpression(
  623.                         sprintf("%s.namespace"$alias),
  624.                         sprintf("(CASE %s.namespace WHEN '' THEN '' ELSE ':' END)"$alias),
  625.                         sprintf("%s.local_name"$alias)
  626.                     ),
  627.                     $operator,
  628.                     $this->conn->quote($literal)
  629.                 ) ;
  630.             }
  631.             if ('jcr:path' !== $operand->getPropertyName() && 'jcr:uuid' !== $operand->getPropertyName()) {
  632.                 if (is_int($literalOperand->getLiteralValue()) || is_float($literalOperand->getLiteralValue())) {
  633.                     return $this->walkNumComparisonConstraint($operand$literalOperand$operator);
  634.                 }
  635.                 if (is_bool($literalOperand->getLiteralValue())) {
  636.                     return $this->walkBoolComparisonConstraint($operand$literalOperand$operator);
  637.                 }
  638.                 return $this->walkTextComparisonConstraint($operand$literalOperand$operator);
  639.             }
  640.         }
  641.         return sprintf(
  642.             '%s %s %s',
  643.             $this->walkOperand($operator1),
  644.             $operator,
  645.             $this->walkOperand($operator2)
  646.         );
  647.     }
  648.     /**
  649.      * @param QOM\PropertyValueInterface $propertyOperand
  650.      * @param QOM\LiteralInterface $literalOperand
  651.      * @param string $operator
  652.      *
  653.      * @return string
  654.      */
  655.     public function walkTextComparisonConstraint(QOM\PropertyValueInterface $propertyOperandQOM\LiteralInterface $literalOperand$operator)
  656.     {
  657.         $alias $this->getTableAlias($propertyOperand->getSelectorName() . '.' $propertyOperand->getPropertyName());
  658.         $property $propertyOperand->getPropertyName();
  659.         return $this->sqlXpathComparePropertyValue($alias$property$this->getLiteralValue($literalOperand), $operator);
  660.     }
  661.     public function walkBoolComparisonConstraint(QOM\PropertyValueInterface $propertyOperandQOM\LiteralInterface $literalOperand$operator)
  662.     {
  663.         $value true === $literalOperand->getLiteralValue() ? '1' '0';
  664.         return $this->walkOperand($propertyOperand) . ' ' $operator ' ' $this->conn->quote($value);
  665.     }
  666.     public function walkNumComparisonConstraint(QOM\PropertyValueInterface $propertyOperandQOM\LiteralInterface $literalOperand$operator)
  667.     {
  668.         $alias $this->getTableAlias($propertyOperand->getSelectorName() . '.' $propertyOperand->getPropertyName());
  669.         $property $propertyOperand->getPropertyName();
  670.         if ($this->platform instanceof MySQLPlatform && '=' === $operator) {
  671.             return sprintf(
  672.                 "0 != FIND_IN_SET('%s', REPLACE(EXTRACTVALUE(%s.props, '//sv:property[@sv:name=%s]/sv:value'), ' ', ','))",
  673.                 $literalOperand->getLiteralValue(),
  674.                 $alias,
  675.                 Xpath::escape($property)
  676.             );
  677.         }
  678.         if ('=' === $operator) {
  679.             return $this->sqlXpathComparePropertyValue($alias$property$literalOperand->getLiteralValue(), $operator);
  680.         }
  681.         return sprintf(
  682.             '%s %s %s',
  683.             $this->sqlXpathExtractNumValue($alias$property),
  684.             $operator,
  685.             $literalOperand->getLiteralValue()
  686.         );
  687.     }
  688.     /**
  689.      * @param string $operator
  690.      *
  691.      * @return string
  692.      */
  693.     public function walkOperator($operator)
  694.     {
  695.         if ($operator === QOM\QueryObjectModelConstantsInterface::JCR_OPERATOR_EQUAL_TO) {
  696.             return '=';
  697.         }
  698.         if ($operator === QOM\QueryObjectModelConstantsInterface::JCR_OPERATOR_GREATER_THAN) {
  699.             return '>';
  700.         }
  701.         if ($operator === QOM\QueryObjectModelConstantsInterface::JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO) {
  702.             return '>=';
  703.         }
  704.         if ($operator === QOM\QueryObjectModelConstantsInterface::JCR_OPERATOR_LESS_THAN) {
  705.             return '<';
  706.         }
  707.         if ($operator === QOM\QueryObjectModelConstantsInterface::JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO) {
  708.             return '<=';
  709.         }
  710.         if ($operator === QOM\QueryObjectModelConstantsInterface::JCR_OPERATOR_NOT_EQUAL_TO) {
  711.             return '!=';
  712.         }
  713.         if ($operator === QOM\QueryObjectModelConstantsInterface::JCR_OPERATOR_LIKE) {
  714.             return 'LIKE';
  715.         }
  716.         return $operator// no-op for simplicity, not standard conform (but using the constants is a pain)
  717.     }
  718.     /**
  719.      * @param QOM\OperandInterface $operand
  720.      *
  721.      * @return string
  722.      *
  723.      * @throws InvalidQueryException
  724.      */
  725.     public function walkOperand(QOM\OperandInterface $operand)
  726.     {
  727.         if ($operand instanceof QOM\NodeNameInterface) {
  728.             $selectorName $operand->getSelectorName();
  729.             $alias $this->getTableAlias($selectorName);
  730.             return $this->platform->getConcatExpression(
  731.                 sprintf("%s.namespace"$alias),
  732.                 sprintf("(CASE %s.namespace WHEN '' THEN '' ELSE ':' END)"$alias),
  733.                 sprintf("%s.local_name"$alias)
  734.             );
  735.         }
  736.         if ($operand instanceof QOM\NodeLocalNameInterface) {
  737.             $selectorName $operand->getSelectorName();
  738.             $alias $this->getTableAlias($selectorName);
  739.             return sprintf("%s.local_name"$alias);
  740.         }
  741.         if ($operand instanceof QOM\LowerCaseInterface) {
  742.             return $this->platform->getLowerExpression($this->walkOperand($operand->getOperand()));
  743.         }
  744.         if ($operand instanceof QOM\UpperCaseInterface) {
  745.             return $this->platform->getUpperExpression($this->walkOperand($operand->getOperand()));
  746.         }
  747.         if ($operand instanceof QOM\LiteralInterface) {
  748.             return $this->conn->quote($this->getLiteralValue($operand));
  749.         }
  750.         if ($operand instanceof QOM\PropertyValueInterface) {
  751.             $alias $this->getTableAlias($operand->getSelectorName() . '.' $operand->getPropertyName());
  752.             $property $operand->getPropertyName();
  753.             if ($property === 'jcr:path') {
  754.                 return sprintf("%s.path"$alias);
  755.             }
  756.             if ($property === "jcr:uuid") {
  757.                 return sprintf("%s.identifier"$alias);
  758.             }
  759.             return $this->sqlXpathExtractValue($alias$property);
  760.         }
  761.         if ($operand instanceof QOM\LengthInterface) {
  762.             $alias $this->getTableAlias($operand->getPropertyValue()->getSelectorName());
  763.             $property $operand->getPropertyValue()->getPropertyName();
  764.             return $this->sqlXpathExtractValueAttribute($alias$property'length');
  765.         }
  766.         throw new InvalidQueryException(sprintf("Dynamic operand %s not yet supported."get_class($operand)));
  767.     }
  768.     /**
  769.      * @param array $orderings
  770.      *
  771.      * @return string
  772.      */
  773.     public function walkOrderings(array $orderings)
  774.     {
  775.         $sql '';
  776.         foreach ($orderings as $ordering) {
  777.             $sql .= empty($sql) ? 'ORDER BY ' ', ';
  778.             $sql .= $this->walkOrdering($ordering);
  779.         }
  780.         return $sql;
  781.     }
  782.     /**
  783.      * @param QOM\OrderingInterface $ordering
  784.      *
  785.      * @return string
  786.      */
  787.     public function walkOrdering(QOM\OrderingInterface $ordering)
  788.     {
  789.         $direction $ordering->getOrder();
  790.         if ($direction === QOM\QueryObjectModelConstantsInterface::JCR_ORDER_ASCENDING) {
  791.             $direction 'ASC';
  792.         } elseif ($direction === QOM\QueryObjectModelConstantsInterface::JCR_ORDER_DESCENDING) {
  793.             $direction 'DESC';
  794.         }
  795.         $sql $this->walkOperand($ordering->getOperand());
  796.         if ($ordering->getOperand() instanceof QOM\PropertyValueInterface) {
  797.             $operand $ordering->getOperand();
  798.             $property $ordering->getOperand()->getPropertyName();
  799.             if ($property !== 'jcr:path' && $property !== 'jcr:uuid') {
  800.                 $alias $this->getTableAlias($operand->getSelectorName() . '.' $property);
  801.                 $numericalSelector $this->sqlXpathExtractValue($alias$property'numerical_props');
  802.                 $sql sprintf(
  803.                     'CAST(%s AS DECIMAL) %s, %s',
  804.                     $numericalSelector,
  805.                     $direction,
  806.                     $sql
  807.                 );
  808.             }
  809.         }
  810.         $sql .= ' ' .$direction;
  811.         return $sql;
  812.     }
  813.     /**
  814.      * @param QOM\LiteralInterface $operand
  815.      *
  816.      * @return string
  817.      *
  818.      * @throws NamespaceException
  819.      */
  820.     private function getLiteralValue(QOM\LiteralInterface $operand)
  821.     {
  822.         $value $operand->getLiteralValue();
  823.         /**
  824.          * Normalize Dates to UTC
  825.          */
  826.         if ($value instanceof DateTime) {
  827.             $valueUTC = clone($value);
  828.             $valueUTC->setTimezone(new DateTimeZone('UTC'));
  829.             return $valueUTC->format('c');
  830.         }
  831.         return $value;
  832.     }
  833.     /**
  834.      * SQL to execute an XPATH expression checking if the property exist on the node with the given alias.
  835.      *
  836.      * @param string $alias
  837.      * @param string $property
  838.      *
  839.      * @return string
  840.      */
  841.     private function sqlXpathValueExists($alias$property)
  842.     {
  843.         if ($this->platform instanceof MySQLPlatform) {
  844.             return sprintf("EXTRACTVALUE(%s.props, 'count(//sv:property[@sv:name=%s]/sv:value[1])') = 1"$aliasXpath::escape($property));
  845.         }
  846.         if ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
  847.             return sprintf("xpath_exists('//sv:property[@sv:name=%s]/sv:value[1]', CAST(%s.props AS xml), ".$this->sqlXpathPostgreSQLNamespaces().") = 't'"Xpath::escape($property), $alias);
  848.         }
  849.         if ($this->platform instanceof SqlitePlatform) {
  850.             return sprintf("EXTRACTVALUE(%s.props, 'count(//sv:property[@sv:name=%s]/sv:value[1])') = 1"$aliasXpath::escape($property));
  851.         }
  852.         throw new NotImplementedException(sprintf("Xpath evaluations cannot be executed with '%s' yet."$this->platform->getName()));
  853.     }
  854.     /**
  855.      * SQL to execute an XPATH expression extracting the property value on the node with the given alias.
  856.      *
  857.      * @param string $alias
  858.      * @param string $property
  859.      *
  860.      * @return string
  861.      */
  862.     private function sqlXpathExtractValue($alias$property$column 'props')
  863.     {
  864.         if ($this->platform instanceof MySQLPlatform) {
  865.             return sprintf("EXTRACTVALUE(%s.%s, '//sv:property[@sv:name=%s]/sv:value[1]')"$alias$columnXpath::escape($property));
  866.         }
  867.         if ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
  868.             return sprintf("(xpath('//sv:property[@sv:name=%s]/sv:value[1]/text()', CAST(%s.%s AS xml), %s))[1]::text"Xpath::escape($property), $alias$column$this->sqlXpathPostgreSQLNamespaces());
  869.         }
  870.         if ($this->platform instanceof SqlitePlatform) {
  871.             return sprintf("EXTRACTVALUE(%s.%s, '//sv:property[@sv:name=%s]/sv:value[1]')"$alias$columnXpath::escape($property));
  872.         }
  873.         throw new NotImplementedException(sprintf("Xpath evaluations cannot be executed with '%s' yet."$this->platform->getName()));
  874.     }
  875.     private function sqlXpathExtractNumValue($alias$property)
  876.     {
  877.         if ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
  878.             return sprintf("(xpath('//sv:property[@sv:name=%s]/sv:value[1]/text()', CAST(%s.props AS xml), %s))[1]::text::int"Xpath::escape($property), $alias$this->sqlXpathPostgreSQLNamespaces());
  879.         }
  880.         return sprintf('CAST(%s AS DECIMAL)'$this->sqlXpathExtractValue($alias$property));
  881.     }
  882.     private function sqlXpathExtractValueAttribute($alias$property$attribute$valueIndex 1)
  883.     {
  884.         if ($this->platform instanceof MySQLPlatform) {
  885.             return sprintf("EXTRACTVALUE(%s.props, '//sv:property[@sv:name=%s]/sv:value[%d]/@%s')"$aliasXpath::escape($property), $valueIndex$attribute);
  886.         }
  887.         if ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
  888.             return sprintf("CAST((xpath('//sv:property[@sv:name=%s]/sv:value[%d]/@%s', CAST(%s.props AS xml), %s))[1]::text AS bigint)"Xpath::escape($property), $valueIndex$attribute$alias$this->sqlXpathPostgreSQLNamespaces());
  889.         }
  890.         if ($this->platform instanceof SqlitePlatform) {
  891.             return sprintf("EXTRACTVALUE(%s.props, '//sv:property[@sv:name=%s]/sv:value[%d]/@%s')"$aliasXpath::escape($property), $valueIndex$attribute);
  892.         }
  893.         throw new NotImplementedException(sprintf("Xpath evaluations cannot be executed with '%s' yet."$this->platform->getName()));
  894.     }
  895.     /**
  896.      * @param $alias
  897.      * @param $property
  898.      * @param $value
  899.      * @param string $operator
  900.      *
  901.      * @return string
  902.      *
  903.      * @throws NotImplementedException if the storage backend is neither mysql
  904.      *      nor postgres nor sqlite
  905.      */
  906.     private function sqlXpathComparePropertyValue($alias$property$value$operator)
  907.     {
  908.         $expression null;
  909.         if ($this->platform instanceof MySQLPlatform) {
  910.             $expression sprintf("EXTRACTVALUE(%s.props, 'count(//sv:property[@sv:name=%s]/sv:value[text()%%s%%s]) > 0')"$aliasXpath::escape($property));
  911.             // mysql does not escape the backslashes for us, while postgres and sqlite do
  912.             $value Xpath::escapeBackslashes($value);
  913.         } elseif ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
  914.             $expression sprintf("xpath_exists('//sv:property[@sv:name=%s]/sv:value[text()%%s%%s]', CAST(%s.props AS xml), %s) = 't'"Xpath::escape($property), $alias$this->sqlXpathPostgreSQLNamespaces());
  915.         } elseif ($this->platform instanceof SqlitePlatform) {
  916.             $expression sprintf("EXTRACTVALUE(%s.props, 'count(//sv:property[@sv:name=%s]/sv:value[text()%%s%%s]) > 0')"$aliasXpath::escape($property));
  917.         } else {
  918.             throw new NotImplementedException(sprintf("Xpath evaluations cannot be executed with '%s' yet."$this->platform->getName()));
  919.         }
  920.         return sprintf($expression$this->walkOperator($operator), Xpath::escape($value));
  921.     }
  922.     /**
  923.      * @return string
  924.      */
  925.     private function sqlXpathPostgreSQLNamespaces()
  926.     {
  927.         return "ARRAY[ARRAY['sv', 'http://www.jcp.org/jcr/sv/1.0']]";
  928.     }
  929.     /**
  930.      * @param QOM\SelectorInterface $source
  931.      * @param string                $alias
  932.      *
  933.      * @return string
  934.      */
  935.     private function sqlNodeTypeClause($aliasQOM\SelectorInterface $source)
  936.     {
  937.         $sql sprintf("%s.type IN ('%s'"$alias$source->getNodeTypeName());
  938.         $subTypes $this->nodeTypeManager->getSubtypes($source->getNodeTypeName());
  939.         foreach ($subTypes as $subType) {
  940.             /* @var $subType NodeTypeInterface */
  941.             $sql .= sprintf(", '%s'"$subType->getName());
  942.         }
  943.         $sql .= ')';
  944.         return $sql;
  945.     }
  946. }