Skip to content
This repository was archived by the owner on Jul 27, 2022. It is now read-only.

ISAICP-6242: Support NOT EXISTS in SPARQL. #2435

Merged
merged 12 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,8 @@
"drupal/sparql_entity_storage": {
"Bundle classes support @see https://github.com/ec-europa/sparql_entity_storage/pull/11": "https://patch-diff.githubusercontent.com/raw/ec-europa/sparql_entity_storage/pull/11.diff",
"Allow routes to filter by bundle @see https://github.com/ec-europa/sparql_entity_storage/pull/32": "https://patch-diff.githubusercontent.com/raw/ec-europa/sparql_entity_storage/pull/32.diff",
"Allow to pass options to the serializer @see https://github.com/idimopoulos/sparql_entity_storage/pull/3": "https://patch-diff.githubusercontent.com/raw/idimopoulos/sparql_entity_storage/pull/3.diff"
"Allow to pass options to the serializer @see https://github.com/idimopoulos/sparql_entity_storage/pull/3": "https://patch-diff.githubusercontent.com/raw/idimopoulos/sparql_entity_storage/pull/3.diff",
"Support NOT EXISTS query @see https://github.com/idimopoulos/sparql_entity_storage/pull/4": "resources/patch/sparql_entity_storage_support_not_exists.patch"
},
"drupal/subpathauto": {
"Subpaths with redirect not resolved @see https://www.drupal.org/project/subpathauto/issues/3175637": "https://git.drupalcode.org/project/subpathauto/-/merge_requests/1/diffs.diff"
Expand Down
15 changes: 12 additions & 3 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

193 changes: 193 additions & 0 deletions resources/patch/sparql_entity_storage_support_not_exists.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
diff --git a/src/Entity/Query/Sparql/Query.php b/src/Entity/Query/Sparql/Query.php
index 9215946..3097e69 100644
--- a/src/Entity/Query/Sparql/Query.php
+++ b/src/Entity/Query/Sparql/Query.php
@@ -355,6 +355,16 @@ class Query extends QueryBase implements SparqlQueryInterface {
if (!in_array($direction, ['ASC', 'DESC'])) {
throw new \RuntimeException('Only "ASC" and "DESC" are allowed as sort order.');
}
+
+ // Unlike the normal SQL queries where a column not defined can be used for
+ // sorting if exists in the table, in SPARQL, the sort argument must be
+ // defined in the WHERE clause. Any sort property, therefore, must will be
+ // included with an EXISTS condition.
+ // Also, the $idKey and $bundleKey properties cannot be added as triples as
+ // they cannot be the object of the triple.
+ if (!in_array($field, [$this->idKey, $this->bundleKey])) {
+ $this->exists($field);
+ }
return parent::sort($field, $direction, $langcode);
}

diff --git a/src/Entity/Query/Sparql/SparqlCondition.php b/src/Entity/Query/Sparql/SparqlCondition.php
index 582416f..cd6de5b 100644
--- a/src/Entity/Query/Sparql/SparqlCondition.php
+++ b/src/Entity/Query/Sparql/SparqlCondition.php
@@ -70,7 +70,7 @@ class SparqlCondition extends ConditionFundamentals implements SparqlConditionIn
'ENDS WITH' => ['prefix' => 'FILTER(STRENDS(', 'suffix' => '))'],
'LIKE' => ['prefix' => 'FILTER(CONTAINS(', 'suffix' => '))'],
'NOT LIKE' => ['prefix' => 'FILTER(!CONTAINS(', 'suffix' => '))'],
- 'EXISTS' => ['prefix' => 'FILTER EXISTS {', 'suffix' => '}'],
+ 'EXISTS' => ['prefix' => '', 'suffix' => ''],
'NOT EXISTS' => ['prefix' => 'FILTER NOT EXISTS {', 'suffix' => '}'],
'<' => ['prefix' => '', 'suffix' => ''],
'>' => ['prefix' => '', 'suffix' => ''],
@@ -272,7 +272,7 @@ class SparqlCondition extends ConditionFundamentals implements SparqlConditionIn
'column' => $column,
];

- if (!in_array($operator, ['EXISTS', 'NOT EXISTS'])) {
+ if ($operator !== 'NOT EXISTS') {
$this->requiresDefaultPattern = FALSE;
}
}
@@ -384,6 +384,7 @@ class SparqlCondition extends ConditionFundamentals implements SparqlConditionIn
}

$mappings = $this->fieldHandler->getFieldPredicates($entity_type_id, $field, $column);
+ $mappings = array_values(array_unique($mappings));
$field_name = $field . '__' . $column;
if (count($mappings) === 1) {
$this->fieldMappings[$field_name] = reset($mappings);
@@ -396,10 +397,10 @@ class SparqlCondition extends ConditionFundamentals implements SparqlConditionIn
// loaded by the database. There is no way that in a single request,
// the same predicate is found with a single and multiple mappings.
// There is no filter per bundle in the query.
- $this->fieldMappingConditions[] = [
+ $this->fieldMappingConditions[$field_name] = [
'field' => $field,
'column' => $column,
- 'value' => array_values(array_unique($mappings)),
+ 'value' => $mappings,
'operator' => 'IN',
];
}
@@ -445,10 +446,11 @@ class SparqlCondition extends ConditionFundamentals implements SparqlConditionIn
$field_name = $condition['field'] . '__' . $condition['column'];
$field_predicate = $this->fieldMappings[$field_name];
$condition_string = self::ID_KEY . ' ' . $this->escapePredicate($field_predicate) . ' ' . SparqlArg::toVar($field_name);
+ $this->addConditionFragment($condition_string);

$condition['value'] = SparqlArg::toResourceUris($condition['value']);
$condition['field'] = $field_predicate;
- $condition_string .= ' . ' . $this->compileValuesFilter($condition);
+ $condition_string = $this->compileValuesFilter($condition);
$this->addConditionFragment($condition_string);
}
}
@@ -503,8 +505,11 @@ class SparqlCondition extends ConditionFundamentals implements SparqlConditionIn
break;

case 'EXISTS':
+ $this->compileExists($condition);
+ break;
+
case 'NOT EXISTS':
- $this->addConditionFragment($this->compileExists($condition));
+ $this->compileNotExists($condition);
break;

case 'CONTAINS':
@@ -589,18 +594,66 @@ class SparqlCondition extends ConditionFundamentals implements SparqlConditionIn
}

/**
- * Compiles a filter exists (or not exists) condition.
+ * Compiles a filter exists condition.
+ *
+ * Since a triple in SPARQL works just like EXISTS does, for EXISTS we add
+ * any condition missing from the field mapping fragments.
*
* @param array $condition
* An array that contains the 'field', 'value', 'operator' values.
+ */
+ protected function compileExists(array $condition): void {
+ $field_predicate = $this->fieldMappings[$condition['field']];
+ $condition_strings = [];
+ $condition_strings[] = self::ID_KEY . ' ' . $this->escapePredicate($field_predicate) . ' ' . SparqlArg::toVar($condition['field']);
+
+ if (isset($this->fieldMappingConditions[$condition['field']])) {
+ $mapping_condition = $this->fieldMappingConditions[$condition['field']];
+ $mapping_condition['value'] = SparqlArg::toResourceUris($mapping_condition['value']);
+ $mapping_condition['field'] = $field_predicate;
+ $condition_strings[] = $this->compileValuesFilter($mapping_condition);
+ }
+
+ foreach ($condition_strings as $condition_string) {
+ if (array_search($condition_string, $this->conditionFragments) === FALSE) {
+ $this->addConditionFragment($condition_string);
+ }
+ }
+ }
+
+ /**
+ * Compiles a filter not exists condition.
*
- * @return string
- * A condition fragment string.
+ * @param array $condition
+ * An array that contains the 'field', 'value', 'operator' values.
*/
- protected function compileExists(array $condition): string {
+ protected function compileNotExists(array $condition): void {
$prefix = self::$filterOperatorMap[$condition['operator']]['prefix'];
$suffix = self::$filterOperatorMap[$condition['operator']]['suffix'];
- return $prefix . self::ID_KEY . ' ' . $this->escapePredicate($this->fieldMappings[$condition['field']]) . ' ' . SparqlArg::toVar($condition['field']) . $suffix;
+
+ $field_predicate = $this->fieldMappings[$condition['field']];
+ $condition_strings = [];
+ $condition_strings[] = self::ID_KEY . ' ' . $this->escapePredicate($field_predicate) . ' ' . SparqlArg::toVar($condition['field']);
+
+ if (isset($this->fieldMappingConditions[$condition['field']])) {
+ $mapping_condition = $this->fieldMappingConditions[$condition['field']];
+ $mapping_condition['value'] = SparqlArg::toResourceUris($mapping_condition['value']);
+ $mapping_condition['field'] = $field_predicate;
+ $condition_strings[] = $this->compileValuesFilter($mapping_condition);
+ }
+
+ foreach ($condition_strings as $condition_string) {
+ $key = array_search($condition_string, $this->conditionFragments);
+ // Since field mapping conditions act also as EXISTS (the triple patterns
+ // MUST exist), remove any pattern added in the mapping conditions so that
+ // only the negative condition below exists.
+ if ($key !== FALSE) {
+ unset($this->conditionFragments[$key]);
+ }
+ }
+
+ $this->addConditionFragment($prefix . implode(' . ', $condition_strings) . $suffix);
+
}

/**
diff --git a/tests/src/Kernel/SparqlEntityQueryTest.php b/tests/src/Kernel/SparqlEntityQueryTest.php
index a8fe4b7..ace3e25 100644
--- a/tests/src/Kernel/SparqlEntityQueryTest.php
+++ b/tests/src/Kernel/SparqlEntityQueryTest.php
@@ -448,6 +448,26 @@ class SparqlEntityQueryTest extends SparqlKernelTestBase {
];
}

+ /**
+ * Tests the NOT EXISTS operator.
+ */
+ public function testNotExists() {
+ $entity = SparqlTest::create([
+ 'id' => 'http://fruit.example.com/not_exists',
+ 'title' => 'fruit title not exists',
+ 'type' => 'fruit',
+ ]);
+ $entity->save();
+ $this->entities[] = $entity;
+
+ $results = $this->getQuery()
+ ->condition('type', 'fruit')
+ ->notExists('text')
+ ->execute();
+
+ $this->assertNotEmpty($results);
+ }
+
/**
* Asserts that arrays are identical.
*/