From 07340752506422a1bebc5cae208ee88489c33c37 Mon Sep 17 00:00:00 2001 From: Vlad Byndych Date: Thu, 12 Oct 2023 17:03:53 +0200 Subject: [PATCH] feat: extended complex search functionality to fetch lists data. --- .../Graph/BasicTransactionManager.php | 106 ++++++++++++++++++ .../Graph/GraphTransactionException.php | 29 +++++ .../Graph/NestedTransactionWrapper.php | 90 +++++++++++++++ .../Graph/TransactionManagerInterface.php | 67 +++++++++++ common/persistence/class.GraphPersistence.php | 47 ++++++-- common/persistence/class.SqlPersistence.php | 2 +- .../persistence/interface.Transactional.php | 34 ++++++ core/kernel/classes/class.Class.php | 5 + .../persistence/interface.ClassInterface.php | 10 ++ .../persistence/smoothsql/class.Class.php | 33 ++++++ .../persistence/smoothsql/search/GateWay.php | 27 +++-- .../smoothsql/search/TaoResultSet.php | 27 ++++- .../persistence/starsql/class.Class.php | 12 +- .../persistence/starsql/class.Resource.php | 23 +++- .../persistence/starsql/search/GateWay.php | 60 +++++++--- .../starsql/search/PropertySerializer.php | 65 +++++++++++ .../starsql/search/QuerySerializer.php | 55 ++++----- 17 files changed, 622 insertions(+), 70 deletions(-) create mode 100644 common/persistence/Graph/BasicTransactionManager.php create mode 100644 common/persistence/Graph/GraphTransactionException.php create mode 100644 common/persistence/Graph/NestedTransactionWrapper.php create mode 100644 common/persistence/Graph/TransactionManagerInterface.php create mode 100644 common/persistence/interface.Transactional.php create mode 100644 core/kernel/persistence/starsql/search/PropertySerializer.php diff --git a/common/persistence/Graph/BasicTransactionManager.php b/common/persistence/Graph/BasicTransactionManager.php new file mode 100644 index 000000000..515198466 --- /dev/null +++ b/common/persistence/Graph/BasicTransactionManager.php @@ -0,0 +1,106 @@ +client = $client; + } + + public function beginTransaction(): void + { + try { + $this->transaction = $this->client->beginTransaction(); + } catch (\Throwable $e) { + throw new GraphTransactionException('Transaction was not started.', $e); + } + } + + public function commit(): void + { + try { + if (isset($this->transaction)) { + $this->transaction->commit(); + unset($this->transaction); + } + } catch (\Throwable $e) { + throw new GraphTransactionException('Transaction was not committed.', $e); + } + } + + public function rollback(): void + { + try { + if (isset($this->transaction)) { + $this->transaction->rollback(); + unset($this->transaction); + } + } catch (\Throwable $e) { + throw new GraphTransactionException('Transaction was not rolled back.', $e); + } + } + + public function run(string $statement, iterable $parameters = []): SummarizedResult + { + try { + if (isset($this->transaction)) { + $result = $this->transaction->run($statement, $parameters); + } else { + $result = $this->client->run($statement, $parameters); + } + } catch (\Throwable $e) { + throw new GraphTransactionException( + sprintf('Exception happen during query run: %s.', $e->getMessage()), + $e + ); + } + + return $result; + } + + public function runStatement(Statement $statement): SummarizedResult + { + try { + if (isset($this->transaction)) { + $result = $this->transaction->runStatement($statement); + } else { + $result = $this->client->runStatement($statement); + } + } catch (\Throwable $e) { + throw new GraphTransactionException( + sprintf('Exception happen during statement run: %s.', $e->getMessage()), + $e + ); + } + + return $result; + } +} diff --git a/common/persistence/Graph/GraphTransactionException.php b/common/persistence/Graph/GraphTransactionException.php new file mode 100644 index 000000000..ac3f80b70 --- /dev/null +++ b/common/persistence/Graph/GraphTransactionException.php @@ -0,0 +1,29 @@ +nestedTransactionManager = $nestedManager; + } + + public function beginTransaction(): void + { + $this->transactionNestingLevel++; + + if ($this->transactionNestingLevel === 1) { + $this->nestedTransactionManager->beginTransaction(); + } + } + + public function commit(): void + { + if ($this->transactionNestingLevel === 0) { + throw new GraphTransactionException('Transaction should be started first.'); + } + + if ($this->isRollbackOnly) { + throw new GraphTransactionException( + 'Nested transaction failed, so all data should be rolled back now.' + ); + } + + if ($this->transactionNestingLevel === 1) { + $this->nestedTransactionManager->commit(); + } + + $this->transactionNestingLevel--; + } + + public function rollback(): void + { + if ($this->transactionNestingLevel === 0) { + throw new GraphTransactionException('Transaction should be started first.'); + } + + if ($this->transactionNestingLevel === 1) { + $this->nestedTransactionManager->rollBack(); + $this->isRollbackOnly = false; + } else { + $this->isRollbackOnly = true; + } + + $this->transactionNestingLevel--; + } + + public function run(string $statement, iterable $parameters = []): SummarizedResult + { + return $this->nestedTransactionManager->run($statement, $parameters); + } + + public function runStatement(Statement $statement): SummarizedResult + { + return $this->nestedTransactionManager->runStatement($statement); + } +} diff --git a/common/persistence/Graph/TransactionManagerInterface.php b/common/persistence/Graph/TransactionManagerInterface.php new file mode 100644 index 000000000..f38948fd1 --- /dev/null +++ b/common/persistence/Graph/TransactionManagerInterface.php @@ -0,0 +1,67 @@ +getDriver()->getClient(); + return $this->getConnection()->run($statement, $parameters); + } + + public function runStatement(Statement $statement): SummarizedResult + { + return $this->getConnection()->runStatement($statement); + } + + public function transactional(Closure $func) + { + $transactionManager = $this->getConnection(); + + $transactionManager->beginTransaction(); + try { + $res = $func(); + $transactionManager->commit(); + + return $res; + } catch (\Throwable $e) { + $transactionManager->rollBack(); - return $client->run($statement, $parameters); + throw $e; + } } - public function runStatement(Statement $statement) + private function getConnection(): TransactionManagerInterface { - /** @var ClientInterface $client */ - $client = $this->getDriver()->getClient(); + if (!isset($this->transactionManager)) { + /** @var ClientInterface $client */ + $client = $this->getDriver()->getClient(); + $this->transactionManager = new NestedTransactionWrapper( + new BasicTransactionManager($client) + ); + } - return $client->runStatement($statement); + return $this->transactionManager; } } diff --git a/common/persistence/class.SqlPersistence.php b/common/persistence/class.SqlPersistence.php index d7407b53d..a06075495 100755 --- a/common/persistence/class.SqlPersistence.php +++ b/common/persistence/class.SqlPersistence.php @@ -27,7 +27,7 @@ /** * Persistence base on SQL */ -class common_persistence_SqlPersistence extends common_persistence_Persistence +class common_persistence_SqlPersistence extends common_persistence_Persistence implements common_persistence_Transactional { /** * @return common_persistence_sql_SchemaManager diff --git a/common/persistence/interface.Transactional.php b/common/persistence/interface.Transactional.php new file mode 100644 index 000000000..d534ae345 --- /dev/null +++ b/common/persistence/interface.Transactional.php @@ -0,0 +1,34 @@ +getServiceManager()->getContainer()->get(ClassRepository::class); } + + public function updateUri(string $newUri) + { + return $this->getImplementation()->updateUri($this, $newUri); + } } diff --git a/core/kernel/persistence/interface.ClassInterface.php b/core/kernel/persistence/interface.ClassInterface.php index bcb6c253f..2c3c8d975 100644 --- a/core/kernel/persistence/interface.ClassInterface.php +++ b/core/kernel/persistence/interface.ClassInterface.php @@ -222,4 +222,14 @@ public function createInstanceWithProperties(core_kernel_classes_Class $type, $p * @return boolean */ public function deleteInstances(core_kernel_classes_Class $resource, $resources, $deleteReference = false); + + /** + * Changes class URI for all its properties and linked objects. + * + * @param core_kernel_classes_Class $resource + * @param string $newUri + * + * @return void + */ + public function updateUri(core_kernel_classes_Class $resource, string $newUri); } diff --git a/core/kernel/persistence/smoothsql/class.Class.php b/core/kernel/persistence/smoothsql/class.Class.php index b27762948..ae2f69d30 100644 --- a/core/kernel/persistence/smoothsql/class.Class.php +++ b/core/kernel/persistence/smoothsql/class.Class.php @@ -595,4 +595,37 @@ public function getFilteredQuery(core_kernel_classes_Class $resource, $propertyF return $query; } + + public function updateUri(core_kernel_classes_Class $resource, string $newUri) + { + $query = $this->getPersistence()->getPlatForm()->getQueryBuilder(); + + $expressionBuilder = $query->expr(); + + $query + ->update('statements') + ->set('subject', ':uri') + ->where($expressionBuilder->eq('subject', ':original_uri')); + + $this->getPersistence()->exec( + $query, + [ + 'uri' => $newUri, + 'original_uri' => $resource->getUri(), + ] + ); + + $query + ->update('statements') + ->set('object', ':uri') + ->where($expressionBuilder->eq('object', ':original_uri')); + + $this->getPersistence()->exec( + $query, + [ + 'uri' => $newUri, + 'original_uri' => $resource->getUri(), + ] + ); + } } diff --git a/core/kernel/persistence/smoothsql/search/GateWay.php b/core/kernel/persistence/smoothsql/search/GateWay.php index c0f7fec42..6bcdd0681 100644 --- a/core/kernel/persistence/smoothsql/search/GateWay.php +++ b/core/kernel/persistence/smoothsql/search/GateWay.php @@ -31,6 +31,7 @@ use oat\oatbox\service\ServiceManager; use oat\search\base\exception\SearchGateWayExeption; use oat\search\base\QueryBuilderInterface; +use oat\search\base\ResultSetInterface; use oat\search\TaoSearchGateWay; /** @@ -106,6 +107,22 @@ public function search(QueryBuilderInterface $Builder) return $resultSet; } + /** + * @param QueryBuilderInterface $Builder + * @param string $propertyUri + * @param bool $isDistinct + * + * @return ResultSetInterface + */ + public function searchTriples(QueryBuilderInterface $Builder, string $propertyUri, bool $isDistinct = false) + { + $statement = $this->connector->query(parent::searchTriples($Builder, $propertyUri, $isDistinct)); + $result = $this->statementToArray($statement); + $resultSet = new $this->resultSetClassName($result, count($result)); + $resultSet->setIsTriple(true); + return $resultSet; + } + /** * * @param Statement $statement @@ -162,14 +179,4 @@ public function join(QueryJoiner $joiner) $resultSet->setParent($this)->setCountQuery($queryCount); return $resultSet; } - - /** - * return parsed query as string - * @return $this - */ - public function printQuery() - { - echo $this->parsedQuery; - return $this; - } } diff --git a/core/kernel/persistence/smoothsql/search/TaoResultSet.php b/core/kernel/persistence/smoothsql/search/TaoResultSet.php index c6429564d..de65aee77 100644 --- a/core/kernel/persistence/smoothsql/search/TaoResultSet.php +++ b/core/kernel/persistence/smoothsql/search/TaoResultSet.php @@ -24,6 +24,8 @@ use oat\search\base\ResultSetInterface; use oat\search\ResultSet; +use function PHPUnit\Framework\returnArgument; + /** * Complex Search resultSet iterator * @@ -40,6 +42,7 @@ class TaoResultSet extends ResultSet implements ResultSetInterface, \oat\search\ */ protected $countQuery; protected $totalCount = null; + private bool $isTriple = false; public function setCountQuery($query) { @@ -47,6 +50,11 @@ public function setCountQuery($query) return $this; } + public function setIsTriple(bool $isTriple) + { + $this->isTriple = $isTriple; + } + /** * return total number of result * @return integer @@ -64,11 +72,26 @@ public function total() /** * return a new resource create from current subject - * @return core_kernel_classes_Resource + * @return core_kernel_classes_Resource|\core_kernel_classes_Triple */ public function current() { $index = parent::current(); - return $this->getResource($index->subject); + if ($this->isTriple) { + return $this->getTriple($index); + } else { + return $this->getResource($index->subject); + } + } + + private function getTriple($row): \core_kernel_classes_Triple + { + $triple = new \core_kernel_classes_Triple(); + + $triple->id = $row->id ?? 0; + $triple->subject = $row->subject ?? ''; + $triple->object = $row->object ?? $row->subject; + + return $triple; } } diff --git a/core/kernel/persistence/starsql/class.Class.php b/core/kernel/persistence/starsql/class.Class.php index ac9fe9f86..8e7e8a3b5 100644 --- a/core/kernel/persistence/starsql/class.Class.php +++ b/core/kernel/persistence/starsql/class.Class.php @@ -490,9 +490,19 @@ private function getClassFilter(array $options, core_kernel_classes_Class $resou $queryOptions['type'] = [ 'resource' => $resource, 'recursive' => $options['recursive'] ?? false, - 'extraClassUriList' => $rdftypes + 'extraClassUriList' => $rdftypes, ]; return $queryOptions; } + + public function updateUri(core_kernel_classes_Class $resource, string $newUri) + { + $query = <<getPersistence()->run($query, ['original_uri' => $resource->getUri(), 'uri' => $newUri]); + } } diff --git a/core/kernel/persistence/starsql/class.Resource.php b/core/kernel/persistence/starsql/class.Resource.php index 223b538fe..7b452aac8 100644 --- a/core/kernel/persistence/starsql/class.Resource.php +++ b/core/kernel/persistence/starsql/class.Resource.php @@ -31,6 +31,7 @@ use function WikibaseSolutions\CypherDSL\query; use function WikibaseSolutions\CypherDSL\parameter; use function WikibaseSolutions\CypherDSL\procedure; +use function WikibaseSolutions\CypherDSL\raw; use function WikibaseSolutions\CypherDSL\relationshipTo; use function WikibaseSolutions\CypherDSL\variable; @@ -334,7 +335,27 @@ public function removePropertyValues(core_kernel_classes_Resource $resource, cor public function removePropertyValueByLg(core_kernel_classes_Resource $resource, core_kernel_classes_Property $property, $lg, $options = []): ?bool { - throw new common_Exception('Not implemented! ' . __FILE__ . ' line: ' . __LINE__); + if (!$property->isLgDependent()) { + return $this->removePropertyValues($resource, $property, $options); + } + + $node = node('Resource')->withProperties(['uri' => $uriParameter = parameter()]); + $property = $node->property($property->getUri()); + $removeKeyProcedure = raw(sprintf( + "[item in %s WHERE NOT item ENDS WITH '@%s']", + $property->toQuery(), + $lg + )); + + $query = query() + ->match($node) + ->where($property->isNotNull()) + ->set($property->replaceWith($removeKeyProcedure)) + ->build(); + + $this->getPersistence()->run($query, [$uriParameter->getParameter() => $resource->getUri()]); + + return true; } public function getRdfTriples(core_kernel_classes_Resource $resource): core_kernel_classes_ContainerCollection diff --git a/core/kernel/persistence/starsql/search/GateWay.php b/core/kernel/persistence/starsql/search/GateWay.php index 104844364..f801c3ab9 100644 --- a/core/kernel/persistence/starsql/search/GateWay.php +++ b/core/kernel/persistence/starsql/search/GateWay.php @@ -26,6 +26,7 @@ use oat\oatbox\service\ServiceManager; use oat\search\base\exception\SearchGateWayExeption; use oat\search\base\QueryBuilderInterface; +use oat\search\base\ResultSetInterface; use oat\search\ResultSet; use oat\search\TaoSearchGateWay; @@ -76,13 +77,55 @@ public function connect() public function search(QueryBuilderInterface $Builder) { - $this->serialyse($Builder); - $result = $this->fetchObjectList($this->parsedQuery); + $result = $this->fetchObjectList(parent::search($Builder)); $totalCount = $this->count($Builder); return new $this->resultSetClassName($result, $totalCount); } + /** + * @param QueryBuilderInterface $Builder + * @param string $propertyUri + * @param bool $isDistinct + * + * @return ResultSetInterface + */ + public function searchTriples(QueryBuilderInterface $Builder, string $propertyUri, bool $isDistinct = false) + { + $result = $this->fetchTripleList( + parent::searchTriples($Builder, $propertyUri, $isDistinct) + ); + return new $this->resultSetClassName($result, count($result)); + } + + /** + * return total count result + * + * @param QueryBuilderInterface $builder + * + * @return int + */ + public function count(QueryBuilderInterface $builder) + { + return (int)($this->fetchOne(parent::count($builder))); + } + + private function fetchTripleList(Statement $query): array + { + $returnValue = []; + $statement = $this->connector->runStatement($query); + foreach ($statement as $row) { + $triple = new \core_kernel_classes_Triple(); + + $triple->id = $row->get('id') ?? 0; + $triple->subject = $row->get('uri') ?? ''; + $triple->object = $row->get('object'); + + $returnValue[] = $triple; + } + return $returnValue; + } + private function fetchObjectList(Statement $query): array { @@ -104,19 +147,6 @@ private function fetchOne(Statement $query) return $results->first()->current(); } - /** - * return total count result - * - * @param QueryBuilderInterface $builder - * - * @return int - */ - public function count(QueryBuilderInterface $builder) - { - $this->parsedQuery = parent::count($builder); - return (int)($this->fetchOne($this->parsedQuery)); - } - public function getQuery() { if ($this->parsedQuery instanceof Statement) { diff --git a/core/kernel/persistence/starsql/search/PropertySerializer.php b/core/kernel/persistence/starsql/search/PropertySerializer.php new file mode 100644 index 000000000..1aa088937 --- /dev/null +++ b/core/kernel/persistence/starsql/search/PropertySerializer.php @@ -0,0 +1,65 @@ +propertyUri = $propertyUri; + $this->isDistinct = $isDistinct; + } + + protected function buildReturn(Node $subject): void + { + $property = $this->propertyUri; + $returnProperty = ModelManager::getModel()->getProperty($property); + + $predicate = $subject->property($property); + if ($returnProperty->isLgDependent()) { + $predicate = $this->buildLanguagePattern($predicate); + } + + $predicate = Procedure::raw('toStringOrNull', $predicate); + if ($this->isDistinct) { + $predicate = Query::rawExpression(sprintf('DISTINCT %s', $predicate->toQuery())); + $this->returnStatements = [ + $predicate->alias('object') + ]; + } else { + $this->returnStatements = [ + Procedure::raw('elementId', $subject)->alias('id'), + $subject->property('uri')->alias('uri'), + $predicate->alias('object') + ]; + } + } +} diff --git a/core/kernel/persistence/starsql/search/QuerySerializer.php b/core/kernel/persistence/starsql/search/QuerySerializer.php index dcd713851..6acd3b3e6 100644 --- a/core/kernel/persistence/starsql/search/QuerySerializer.php +++ b/core/kernel/persistence/starsql/search/QuerySerializer.php @@ -52,7 +52,7 @@ class QuerySerializer implements QuerySerialyserInterface protected array $whereConditions = []; - private array $returnStatements = []; + protected array $returnStatements = []; protected QueryConvertible $orderCondition; @@ -101,6 +101,15 @@ public function count(bool $count = true): self } } + public function property(string $propertyUri, bool $isDistinct = false): self + { + return (new PropertySerializer($propertyUri, $isDistinct)) + ->setServiceLocator($this->getServiceLocator()) + ->setOptions($this->getOptions()) + ->setDriverEscaper($this->getDriverEscaper()) + ->setCriteriaList($this->criteriaList); + } + public function setOptions(array $options) { $this->defaultLanguage = !empty($options['defaultLanguage']) @@ -134,8 +143,8 @@ public function serialyse() if ($this->criteriaList->getLimit() > 0) { $query - ->skip($this->criteriaList->getOffset()) - ->limit($this->criteriaList->getLimit()); + ->skip((int)$this->criteriaList->getOffset()) + ->limit((int)$this->criteriaList->getLimit()); } return Statement::create($query->build(), $this->parameters); @@ -167,7 +176,7 @@ protected function buildMatchPatterns(Node $subject): void ->withVariable(Query::variable('grandParent')); $subClassRelation = Query::relationshipTo() ->addType(OntologyRdfs::RDFS_SUBCLASSOF) - ->withArbitraryHops(); + ->withMinHops(0); $parentPath = $parentPath->relationship($subClassRelation, $grandParentClass); $parentWhere = $parentWhere->or( @@ -222,10 +231,14 @@ protected function buildWhereConditions(Node $subject): void protected function buildCondition(QueryCriterionInterface $operation, Node $subject): BooleanType { - $property = ModelManager::getModel()->getProperty($operation->getName()); + $propertyName = $operation->getName() === QueryCriterionInterface::VIRTUAL_URI_FIELD + ? 'uri' + : $operation->getName(); + + $property = ModelManager::getModel()->getProperty($propertyName); if ($property->isRelationship()) { $object = Query::node('Resource'); - $this->matchPatterns[] = $subject->relationshipTo($object, $operation->getName()); + $this->matchPatterns[] = $subject->relationshipTo($object, $propertyName); $predicate = $object->property('uri'); $values = $operation->getValue(); @@ -236,7 +249,7 @@ protected function buildCondition(QueryCriterionInterface $operation, Node $subj is_array($values) ? SupportedOperatorHelper::IN : SupportedOperatorHelper::EQUAL ); } else { - $predicate = $subject->property($operation->getName()); + $predicate = $subject->property($propertyName); if ($property->isLgDependent()) { $predicate = $this->buildLanguagePattern($predicate); } @@ -266,7 +279,7 @@ protected function buildPropertyQuery( return $condition->getCondition(); } - private function buildLanguagePattern(QueryConvertible $predicate): RawExpression + protected function buildLanguagePattern(QueryConvertible $predicate): RawExpression { if (empty($this->userLanguage) || $this->userLanguage === $this->defaultLanguage) { $resultExpression = Query::rawExpression( @@ -291,31 +304,9 @@ private function buildLanguagePattern(QueryConvertible $predicate): RawExpressio return $resultExpression; } - private function buildReturn(Node $subject): void + protected function buildReturn(Node $subject): void { - $queryOptions = $this->criteriaList->getOptions(); - - $isDistinct = $queryOptions['distinct'] ?? false; - - if (isset($queryOptions['return_field'])) { - $property = $queryOptions['return_field']; - $returnProperty = ModelManager::getModel()->getProperty($property); - - $predicate = $subject->property($property); - if ($returnProperty->isLgDependent()) { - $predicate = $this->buildLanguagePattern($predicate); - } - - $predicate = Procedure::raw('toStringOrNull', $predicate); - } else { - $predicate = $subject->property('uri'); - } - - if ($isDistinct) { - $predicate = Query::rawExpression(sprintf('DISTINCT %s', $predicate->toQuery())); - } - - $this->returnStatements[] = $predicate; + $this->returnStatements[] = $subject->property('uri'); } protected function buildOrderCondition(Node $subject): void