summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php')
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php349
1 files changed, 349 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php
new file mode 100644
index 00000000..ad1f65a6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php
@@ -0,0 +1,349 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use RuntimeException;
+use SMW\MediaWiki\Database;
+use SMW\SQLStore\TableBuilder\TemporaryTableBuilder;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class QuerySegmentListProcessor {
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var TemporaryTableBuilder
+ */
+ private $temporaryTableBuilder;
+
+ /**
+ * @var HierarchyTempTableBuilder
+ */
+ private $hierarchyTempTableBuilder;
+
+ /**
+ * Array of arrays of executed queries, indexed by the temporary table names
+ * results were fed into.
+ *
+ * @var array
+ */
+ private $executedQueries = [];
+
+ /**
+ * Query mode copied from given query. Some submethods act differently when
+ * in Query::MODE_DEBUG.
+ *
+ * @var int
+ */
+ private $queryMode;
+
+ /**
+ * @var array
+ */
+ private $querySegmentList = [];
+
+ /**
+ * @param Database $connection
+ * @param TemporaryTableBuilder $temporaryTableBuilder
+ * @param HierarchyTempTableBuilder $hierarchyTempTableBuilder
+ */
+ public function __construct( Database $connection, TemporaryTableBuilder $temporaryTableBuilder, HierarchyTempTableBuilder $hierarchyTempTableBuilder ) {
+ $this->connection = $connection;
+ $this->temporaryTableBuilder = $temporaryTableBuilder;
+ $this->hierarchyTempTableBuilder = $hierarchyTempTableBuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getExecutedQueries() {
+ return $this->executedQueries;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param &$querySegmentList
+ */
+ public function setQuerySegmentList( &$querySegmentList ) {
+ $this->querySegmentList =& $querySegmentList;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param integer
+ */
+ public function setQueryMode( $queryMode ) {
+ $this->queryMode = $queryMode;
+ }
+
+ /**
+ * Process stored queries and change store accordingly. The query obj is modified
+ * so that it contains non-recursive description of a select to execute for getting
+ * the actual result.
+ *
+ * @param integer $id
+ * @throws RuntimeException
+ */
+ public function process( $id ) {
+
+ $this->hierarchyTempTableBuilder->emptyHierarchyCache();
+ $this->executedQueries = [];
+
+ // Should never happen
+ if ( !isset( $this->querySegmentList[$id] ) ) {
+ throw new RuntimeException( "$id doesn't exist" );
+ }
+
+ $this->resolve( $this->querySegmentList[$id] );
+ }
+
+ private function resolve( QuerySegment &$query ) {
+
+ switch ( $query->type ) {
+ case QuerySegment::Q_TABLE: // Normal query with conjunctive subcondition.
+ foreach ( $query->components as $qid => $joinField ) {
+ $subQuery = $this->querySegmentList[$qid];
+ $this->resolve( $subQuery );
+
+ if ( $subQuery->joinTable !== '' ) { // Join with jointable.joinfield
+ $op = $subQuery->not ? '!' : '';
+
+ $joinType = $subQuery->joinType ? $subQuery->joinType : 'INNER';
+ $t = $this->connection->tableName( $subQuery->joinTable ) ." AS $subQuery->alias";
+
+ if ( $subQuery->from ) {
+ $t = "($t $subQuery->from)";
+ }
+
+ $query->from .= " $joinType JOIN $t ON $joinField$op=" . $subQuery->joinfield;
+
+ if ( $joinType === 'LEFT' ) {
+ $query->where .= ( ( $query->where === '' ) ? '' : ' AND ' ) . '(' . $subQuery->joinfield . ' IS NULL)';
+ }
+
+ } elseif ( $subQuery->joinfield !== '' ) { // Require joinfield as "value" via WHERE.
+ $condition = '';
+
+ if ( $subQuery->null === true ) {
+ $condition .= ( $condition ? ' OR ': '' ) . "$joinField IS NULL";
+ } else {
+ foreach ( $subQuery->joinfield as $value ) {
+ $op = $subQuery->not ? '!' : '';
+ $condition .= ( $condition ? ' OR ': '' ) . "$joinField$op=" . $this->connection->addQuotes( $value );
+ }
+ }
+
+ if ( count( $subQuery->joinfield ) > 1 ) {
+ $condition = "($condition)";
+ }
+
+ $query->where .= ( ( $query->where === '' || $subQuery->where === null ) ? '' : ' AND ' ) . $condition;
+ $query->from .= $subQuery->from;
+ } else { // interpret empty joinfields as impossible condition (empty result)
+ $query->joinfield = ''; // make whole query false
+ $query->joinTable = '';
+ $query->where = '';
+ $query->from = '';
+ break;
+ }
+
+ if ( $subQuery->where !== '' && $subQuery->where !== null ) {
+ if ( $subQuery->joinType === 'LEFT' || $subQuery->joinType == 'LEFT OUTER' ) {
+ $query->from .= ' AND (' . $subQuery->where . ')';
+ } else {
+ $query->where .= ( ( $query->where === '' ) ? '' : ' AND ' ) . '(' . $subQuery->where . ')';
+ }
+ }
+ }
+
+ $query->components = [];
+ break;
+ case QuerySegment::Q_CONJUNCTION:
+ reset( $query->components );
+ $key = false;
+
+ // Pick one subquery as anchor point ...
+ foreach ( $query->components as $qkey => $qid ) {
+ $key = $qkey;
+
+ if ( $this->querySegmentList[$qkey]->joinTable !== '' ) {
+ break;
+ }
+ }
+
+ $result = $this->querySegmentList[$key];
+ unset( $query->components[$key] );
+
+ // Execute it first (may change jointable and joinfield, e.g. when making temporary tables)
+ $this->resolve( $result );
+
+ // ... and append to this query the remaining queries.
+ foreach ( $query->components as $qid => $joinfield ) {
+ $result->components[$qid] = $result->joinfield;
+ }
+
+ // Second execute, now incorporating remaining conditions.
+ $this->resolve( $result );
+ $query = $result;
+ break;
+ case QuerySegment::Q_DISJUNCTION:
+ if ( $this->queryMode !== Query::MODE_NONE ) {
+ $this->temporaryTableBuilder->create( $this->connection->tableName( $query->alias ) );
+ }
+
+ $this->executedQueries[$query->alias] = [];
+
+ foreach ( $query->components as $qid => $joinField ) {
+ $subQuery = $this->querySegmentList[$qid];
+ $this->resolve( $subQuery );
+ $sql = '';
+
+ if ( $subQuery->joinTable !== '' ) {
+ $sql = 'INSERT ' . 'IGNORE ' . 'INTO ' .
+ $this->connection->tableName( $query->alias ) .
+ " SELECT DISTINCT $subQuery->joinfield FROM " . $this->connection->tableName( $subQuery->joinTable ) .
+ " AS $subQuery->alias $subQuery->from" . ( $subQuery->where ? " WHERE $subQuery->where":'' );
+ } elseif ( $subQuery->joinfield !== '' ) {
+ // NOTE: this works only for single "unconditional" values without further
+ // WHERE or FROM. The execution must take care of not creating any others.
+ $values = '';
+
+ // This produces an error on postgres with
+ // pg_query(): Query failed: ERROR: duplicate key value violates
+ // unique constraint "sunittest_t3_pkey" DETAIL: Key (id)=(274) already exists.
+
+ foreach ( $subQuery->joinfield as $value ) {
+ $values .= ( $values ? ',' : '' ) . '(' . $this->connection->addQuotes( $value ) . ')';
+ }
+
+ $sql = 'INSERT ' . 'IGNORE ' . 'INTO ' . $this->connection->tableName( $query->alias ) . " (id) VALUES $values";
+ } // else: // interpret empty joinfields as impossible condition (empty result), ignore
+ if ( $sql ) {
+ $this->executedQueries[$query->alias][] = $sql;
+
+ if ( $this->queryMode !== Query::MODE_NONE ) {
+ $this->connection->query(
+ $sql,
+ __METHOD__
+ );
+ }
+ }
+ }
+
+ $query->type = QuerySegment::Q_TABLE;
+ $query->where = '';
+ $query->components = [];
+
+ $query->joinTable = $query->alias;
+ $query->joinfield = "$query->alias.id";
+ $query->sortfields = []; // Make sure we got no sortfields.
+ // TODO: currently this eliminates sortkeys, possibly keep them (needs different temp table format though, maybe not such a good thing to do)
+ break;
+ case QuerySegment::Q_PROP_HIERARCHY:
+ case QuerySegment::Q_CLASS_HIERARCHY: // make a saturated hierarchy
+ $this->resolveHierarchy( $query );
+ break;
+ case QuerySegment::Q_VALUE:
+ break; // nothing to do
+ }
+ }
+
+ /**
+ * Find subproperties or subcategories. This may require iterative computation,
+ * and temporary tables are used in many cases.
+ *
+ * @param QuerySegment $query
+ */
+ private function resolveHierarchy( QuerySegment &$query ) {
+
+ switch ( $query->type ) {
+ case QuerySegment::Q_PROP_HIERARCHY:
+ $type = 'property';
+ break;
+ case QuerySegment::Q_CLASS_HIERARCHY:
+ $type = 'class';
+ break;
+ }
+
+ list( $smwtable, $depth ) = $this->hierarchyTempTableBuilder->getHierarchyTableDefinitionForType(
+ $type
+ );
+
+ // An individual depth was annotated as part of the query
+ if ( $query->depth !== null ) {
+ $depth = $query->depth;
+ }
+
+ if ( $depth <= 0 ) { // treat as value, no recursion
+ $query->type = QuerySegment::Q_VALUE;
+ return;
+ }
+
+ $values = '';
+ $valuecond = '';
+
+ foreach ( $query->joinfield as $value ) {
+ $values .= ( $values ? ',':'' ) . '(' . $this->connection->addQuotes( $value ) . ')';
+ $valuecond .= ( $valuecond ? ' OR ':'' ) . 'o_id=' . $this->connection->addQuotes( $value );
+ }
+
+ // Try to safe time (SELECT is cheaper than creating/dropping 3 temp tables):
+ $res = $this->connection->select(
+ $smwtable,
+ 's_id',
+ $valuecond,
+ __METHOD__,
+ [ 'LIMIT' => 1 ]
+ );
+
+ if ( !$this->connection->fetchObject( $res ) ) { // no subobjects, we are done!
+ $this->connection->freeResult( $res );
+ $query->type = QuerySegment::Q_VALUE;
+ return;
+ }
+
+ $this->connection->freeResult( $res );
+ $tablename = $this->connection->tableName( $query->alias );
+ $this->executedQueries[$query->alias] = [
+ "Recursively computed hierarchy for element(s) $values.",
+ "SELECT s_id FROM $smwtable WHERE $valuecond LIMIT 1"
+ ];
+
+ $query->joinTable = $query->alias;
+ $query->joinfield = "$query->alias.id";
+
+ $this->hierarchyTempTableBuilder->createHierarchyTempTableFor(
+ $type,
+ $tablename,
+ $values,
+ $depth
+ );
+ }
+
+ /**
+ * After querying, make sure no temporary database tables are left.
+ * @todo I might be better to keep the tables and possibly reuse them later
+ * on. Being temporary, the tables will vanish with the session anyway.
+ */
+ public function cleanUp() {
+ foreach ( $this->executedQueries as $table => $log ) {
+ $this->temporaryTableBuilder->drop( $this->connection->tableName( $table ) );
+ }
+ }
+
+}