Skip to content

Commit

Permalink
Allow sorting on numerical columns
Browse files Browse the repository at this point in the history
- New column "numerical_props" which is populated by numerical properies
  and which is used exlusively for ordering.
  • Loading branch information
dantleech committed Dec 28, 2014
1 parent 9cd5e8a commit 27988c9
Show file tree
Hide file tree
Showing 10 changed files with 473 additions and 89 deletions.
141 changes: 103 additions & 38 deletions src/Jackalope/Transport/DoctrineDBAL/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ public function __construct(FactoryInterface $factory, Connection $conn)
private function registerSqliteFunctions(PDOConnection $sqliteConnection)
{
$sqliteConnection->sqliteCreateFunction('EXTRACTVALUE', function ($string, $expression) {
if (null === $string) {
return null;
}

$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->loadXML($string);
$xpath = new \DOMXPath($dom);
Expand Down Expand Up @@ -571,10 +575,20 @@ public function copyNode($srcAbsPath, $dstAbsPath, $srcWorkspace = null)
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
$newPath = str_replace($srcAbsPath, $dstAbsPath, $row['path']);

$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->loadXML($row['props']);
$stringDom = new \DOMDocument('1.0', 'UTF-8');
$stringDom->loadXML($row['props']);

$numericalDom = null;

$propsData = array('dom' => $dom);
if ($row['numerical_props']) {
$numericalDom = new \DOMDocument('1.0', 'UTF-8');
$numericalDom->loadXML($row['numerical_props']);
}

$propsData = array(
'stringDom' => $stringDom,
'numericalDom' => $numericalDom
);
// when copying a node, the copy is always a new node. set $isNewNode to true
$newNodeId = $this->syncNode(null, $newPath, $row['type'], true, array(), $propsData);

Expand Down Expand Up @@ -646,25 +660,27 @@ private function syncNode($uuid, $path, $type, $isNewNode, $props = array(), $pr

$qb = $this->conn->createQueryBuilder();

$qb->select(':identifier, :type, :path, :local_name, :namespace, :parent, :workspace_name, :props, :depth, COALESCE(MAX(n.sort_order), 0) + 1')
$qb->select(':identifier, :type, :path, :local_name, :namespace, :parent, :workspace_name, :props, :numerical_props, :depth, COALESCE(MAX(n.sort_order), 0) + 1')
->from('phpcr_nodes', 'n')
->where('n.parent = :parent_a');

$sql = $qb->getSql();

try {
$insert = "INSERT INTO phpcr_nodes (identifier, type, path, local_name, namespace, parent, workspace_name, props, depth, sort_order) " . $sql;
$this->conn->executeUpdate($insert, array(
'identifier' => $uuid,
'type' => $type,
'path' => $path,
'local_name' => $localName,
'namespace' => $namespace,
'parent' => PathHelper::getParentPath($path),
$insert = "INSERT INTO phpcr_nodes (identifier, type, path, local_name, namespace, parent, workspace_name, props, numerical_props, depth, sort_order) " . $sql;

$this->conn->executeUpdate($insert, $data = array(
'identifier' => $uuid,
'type' => $type,
'path' => $path,
'local_name' => $localName,
'namespace' => $namespace,
'parent' => PathHelper::getParentPath($path),
'workspace_name' => $this->workspaceName,
'props' => $propsData['dom']->saveXML(),
'depth' => PathHelper::getPathDepth($path),
'parent_a' => PathHelper::getParentPath($path),
'props' => $propsData['stringDom']->saveXML(),
'numerical_props' => $propsData['numericalDom'] ? $propsData['numericalDom']->saveXML() : null,
'depth' => PathHelper::getPathDepth($path),
'parent_a' => PathHelper::getParentPath($path),
));
} catch(\Exception $e) {
if ($e instanceof \PDOException || $e instanceof DBALException) {
Expand All @@ -684,7 +700,13 @@ private function syncNode($uuid, $path, $type, $isNewNode, $props = array(), $pr
if (!$nodeId) {
throw new RepositoryException("nodeId for $path not found");
}
$this->conn->update('phpcr_nodes', array('props' => $propsData['dom']->saveXML()), array('id' => $nodeId));

$this->conn->update('phpcr_nodes', array(
'props' => $propsData['stringDom']->saveXML(),
'numerical_props' => $propsData['numericalDom'] ? $propsData['numericalDom']->saveXML() : null,
),
array('id' => $nodeId)
);
}

$this->nodeIdentifiers[$path] = $uuid;
Expand Down Expand Up @@ -853,6 +875,7 @@ public static function xmlToProps($xml, ValueConverter $valueConverter, $filter
}

$values = array();

$type = PropertyType::valueFromName($propertyNode->getAttribute('sv:type'));
foreach ($propertyNode->childNodes as $valueNode) {
switch ($type) {
Expand Down Expand Up @@ -912,7 +935,11 @@ public static function xmlToProps($xml, ValueConverter $valueConverter, $filter
* @param array $properties
* @param boolean $inlineBinaries
*
* @return array ('dom' => $dom, 'binaryData' => streams, 'references' => array('type' => INT, 'values' => array(UUIDs)))
* @return array (
* 'stringDom' => $stringDom,
* 'numericalDom' => $numericalDom',
* 'binaryData' => streams,
* 'references' => array('type' => INT, 'values' => array(UUIDs)))
*/
private function propsToXML($properties, $inlineBinaries = false)
{
Expand All @@ -925,20 +952,16 @@ private function propsToXML($properties, $inlineBinaries = false)
'rep' => "internal",
);

$dom = new \DOMDocument('1.0', 'UTF-8');
$rootNode = $dom->createElement('sv:node');
foreach ($namespaces as $namespace => $uri) {
$rootNode->setAttribute('xmlns:' . $namespace, $uri);
}
$dom->appendChild($rootNode);
$doms = array(
'stringDom' => array(),
'numericalDom' => array(),
);

$binaryData = $references = array();

foreach ($properties as $property) {
/* @var $property Property */
$propertyNode = $dom->createElement('sv:property');
$propertyNode->setAttribute('sv:name', $property->getName());
$propertyNode->setAttribute('sv:type', PropertyType::nameFromValue($property->getType()));
$propertyNode->setAttribute('sv:multi-valued', $property->isMultiple() ? '1' : '0');

$targetDoms = array('stringDom');

switch ($property->getType()) {
case PropertyType::WEAKREFERENCE:
Expand All @@ -955,12 +978,14 @@ private function propsToXML($properties, $inlineBinaries = false)
break;
case PropertyType::DECIMAL:
$values = $property->getDecimal();
$targetDoms[] = 'numericalDom';
break;
case PropertyType::BOOLEAN:
$values = array_map('intval', (array) $property->getBoolean());
break;
case PropertyType::LONG:
$values = $property->getLong();
$targetDoms[] = 'numericalDom';
break;
case PropertyType::BINARY:
if ($property->isNew() || $property->isModified()) {
Expand Down Expand Up @@ -998,26 +1023,66 @@ private function propsToXML($properties, $inlineBinaries = false)
break;
case PropertyType::DOUBLE:
$values = $property->getDouble();
$targetDoms[] = 'numericalDom';
break;
default:
throw new RepositoryException('unknown type '.$property->getType());
}

$lengths = (array) $property->getLength();
foreach ((array) $values as $key => $value) {
$element = $propertyNode->appendChild($dom->createElement('sv:value'));
$element->appendChild($dom->createTextNode($value));
if (isset($lengths[$key])) {
$lengthAttribute = $dom->createAttribute('length');
$lengthAttribute->value = $lengths[$key];
$element->appendChild($lengthAttribute);
foreach ($targetDoms as $targetDom) {
$doms[$targetDom][] = array(
'name' => $property->getName(),
'type' => PropertyType::nameFromValue($property->getType()),
'multiple' => $property->isMultiple(),
'lengths' => (array) $property->getLength(),
'values' => $values,
);
}
}

$ret = array(
'stringDom' => null,
'numericalDom' => null,
'binaryData' => $binaryData,
'references' => $references
);

foreach ($doms as $targetDom => $properties) {

$dom = new \DOMDocument('1.0', 'UTF-8');
$rootNode = $dom->createElement('sv:node');
foreach ($namespaces as $namespace => $uri) {
$rootNode->setAttribute('xmlns:' . $namespace, $uri);
}
$dom->appendChild($rootNode);

foreach ($properties as $property) {

/* @var $property Property */
$propertyNode = $dom->createElement('sv:property');
$propertyNode->setAttribute('sv:name', $property['name']);
$propertyNode->setAttribute('sv:type', $property['type']);
$propertyNode->setAttribute('sv:multi-valued', $property['multiple'] ? '1' : '0');
$lengths = (array) $property['lengths'];
foreach ((array) $property['values'] as $key => $value) {
$element = $propertyNode->appendChild($dom->createElement('sv:value'));
$element->appendChild($dom->createTextNode($value));
if (isset($lengths[$key])) {
$lengthAttribute = $dom->createAttribute('length');
$lengthAttribute->value = $lengths[$key];
$element->appendChild($lengthAttribute);
}
}

$rootNode->appendChild($propertyNode);
}

$rootNode->appendChild($propertyNode);
if (count($properties)) {
$ret[$targetDom] = $dom;
}
}

return array('dom' => $dom, 'binaryData' => $binaryData, 'references' => $references);
return $ret;
}

/**
Expand Down
50 changes: 24 additions & 26 deletions src/Jackalope/Transport/DoctrineDBAL/Query/QOMWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,26 @@ public function walkOrdering(QOM\OrderingInterface $ordering)
$direction = 'DESC';
}

return $this->walkOperand($ordering->getOperand()) . " " . $direction;
$sql = $this->walkOperand($ordering->getOperand());

if ($ordering->getOperand() instanceof QOM\PropertyValueInterface) {
$operand = $ordering->getOperand();
$property = $ordering->getOperand()->getPropertyName();
if ($property !== 'jcr:path' && $property !== 'jcr:uuid') {
$alias = $this->getTableAlias($operand->getSelectorName() . '.' . $property);

$numericalSelector = $this->sqlXpathExtractValue($alias, $property, 'numerical_props');

$sql = sprintf('CAST(%s AS DECIMAL), %s',
$numericalSelector,
$sql
);
}
}

$sql .= ' ' .$direction;

return $sql;
}

/**
Expand Down Expand Up @@ -791,16 +810,16 @@ private function sqlXpathValueExists($alias, $property)
*
* @return string
*/
private function sqlXpathExtractValue($alias, $property)
private function sqlXpathExtractValue($alias, $property, $column = 'props')
{
if ($this->platform instanceof MySqlPlatform) {
return "EXTRACTVALUE($alias.props, '//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]')";
return "EXTRACTVALUE($alias.$column, '//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]')";
}
if ($this->platform instanceof PostgreSqlPlatform) {
return "(xpath('//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]/text()', CAST($alias.props AS xml), ".$this->sqlXpathPostgreSQLNamespaces()."))[1]::text";
return "(xpath('//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]/text()', CAST($alias.$column AS xml), ".$this->sqlXpathPostgreSQLNamespaces()."))[1]::text";
}
if ($this->platform instanceof SqlitePlatform) {
return "EXTRACTVALUE($alias.props, '//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]')";
return "EXTRACTVALUE($alias.$column, '//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]')";
}

throw new NotImplementedException("Xpath evaluations cannot be executed with '" . $this->platform->getName() . "' yet.");
Expand Down Expand Up @@ -868,27 +887,6 @@ private function sqlXpathPostgreSQLNamespaces()
return "ARRAY[ARRAY['sv', 'http://www.jcp.org/jcr/sv/1.0']]";
}

/**
* Returns the SQL part to select the given property
*
* @param string $alias
* @param string $propertyName
*
* @return string
*/
private function sqlProperty($alias, $propertyName)
{
if ('jcr:uuid' === $propertyName) {
return "$alias.identifier";
}

if ('jcr:path' === $propertyName) {
return "$alias.path";
}

return $this->sqlXpathExtractValue($alias, $propertyName);
}

/**
* @param QOM\SelectorInterface $source
* @param string $alias
Expand Down
1 change: 1 addition & 0 deletions src/Jackalope/Transport/DoctrineDBAL/RepositorySchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ protected function addNodesTable()
$nodes->addColumn('identifier', 'string');
$nodes->addColumn('type', 'string');
$nodes->addColumn('props', 'text');
$nodes->addColumn('numerical_props', 'text', array('notnull' => false));
$nodes->addColumn('depth', 'integer');
$nodes->addColumn('sort_order', 'integer', array('notnull' => false));
$nodes->setPrimaryKey(array('id'));
Expand Down
Loading

0 comments on commit 27988c9

Please sign in to comment.