/**
  * Helper function for reading all data for from a given property table
  * (specified by an SMWSQLStore3Table object), based on certain
  * restrictions. The function can filter data based on the subject (1)
  * or on the property it belongs to (2) -- but one of those must be
  * done. The Boolean $issubject is true for (1) and false for (2).
  *
  * In case (1), the first two parameters are taken to refer to a
  * subject; in case (2) they are taken to refer to a property. In any
  * case, the retrieval is limited to the specified $proptable. The
  * parameters are an internal $id (of a subject or property), and an
  * $object (being an DIWikiPage or SMWDIProperty). Moreover, when
  * filtering by property, it is assumed that the given $proptable
  * belongs to the property: if it is a table with fixed property, it
  * will not be checked that this is the same property as the one that
  * was given in $object.
  *
  * In case (1), the result in general is an array of pairs (arrays of
  * size 2) consisting of a property key (string), and DB keys (array if
  * many, string if one) from which a datvalue object for this value can
  * be built. It is possible that some of the DB keys are based on
  * internal objects; these will be represented by similar result arrays
  * of (recursive calls of) fetchSemanticData().
  *
  * In case (2), the result is simply an array of DB keys (array)
  * without the property keys. Container objects will be encoded with
  * nested arrays like in case (1).
  *
  * @todo Maybe share DB handler; asking for it seems to take quite some
  * time and we do not want to change it in one call.
  *
  * @param integer $id
  * @param SMWDataItem $object
  * @param TableDefinition $propTable
  * @param boolean $isSubject
  * @param SMWRequestOptions $requestOptions
  *
  * @return array
  */
 private function fetchSemanticData($id, SMWDataItem $object = null, TableDefinition $propTable, $isSubject = true, SMWRequestOptions $requestOptions = null)
 {
     // stop if there is not enough data:
     // properties always need to be given as object,
     // subjects at least if !$proptable->idsubject
     if ($id == 0 || is_null($object) && (!$isSubject || !$propTable->usesIdSubject())) {
         return array();
     }
     $result = array();
     $db = $this->store->getConnection();
     $diHandler = $this->store->getDataItemHandlerForDIType($propTable->getDiType());
     // ***  First build $from, $select, and $where for the DB query  ***//
     $from = $db->tableName($propTable->getName());
     // always use actual table
     $select = '';
     $where = '';
     if ($isSubject) {
         // restrict subject, select property
         $where .= $propTable->usesIdSubject() ? 's_id=' . $db->addQuotes($id) : 's_title=' . $db->addQuotes($object->getDBkey()) . ' AND s_namespace=' . $db->addQuotes($object->getNamespace());
         if (!$propTable->isFixedPropertyTable()) {
             // select property name
             $from .= ' INNER JOIN ' . $db->tableName(SMWSql3SmwIds::tableName) . ' AS p ON p_id=p.smw_id';
             $select .= 'p.smw_title as prop';
         }
         // else: fixed property, no select needed
     } elseif (!$propTable->isFixedPropertyTable()) {
         // restrict property only
         $where .= 'p_id=' . $db->addQuotes($id);
     }
     $valuecount = 0;
     // Don't use DISTINCT for value of one subject:
     $usedistinct = !$isSubject;
     $valueField = $diHandler->getIndexField();
     $labelField = $diHandler->getLabelField();
     $fields = $diHandler->getFetchFields();
     foreach ($fields as $fieldname => $typeid) {
         // select object column(s)
         if ($typeid == 'p') {
             // get data from ID table
             $from .= ' INNER JOIN ' . $db->tableName(SMWSql3SmwIds::tableName) . " AS o{$valuecount} ON {$fieldname}=o{$valuecount}.smw_id";
             $select .= ($select !== '' ? ',' : '') . "{$fieldname} AS id{$valuecount}" . ",o{$valuecount}.smw_title AS v{$valuecount}" . ",o{$valuecount}.smw_namespace AS v" . ($valuecount + 1) . ",o{$valuecount}.smw_iw AS v" . ($valuecount + 2) . ",o{$valuecount}.smw_sortkey AS v" . ($valuecount + 3) . ",o{$valuecount}.smw_subobject AS v" . ($valuecount + 4);
             if ($valueField == $fieldname) {
                 $valueField = "o{$valuecount}.smw_sortkey";
             }
             if ($labelField == $fieldname) {
                 $labelField = "o{$valuecount}.smw_sortkey";
             }
             $valuecount += 4;
         } else {
             $select .= ($select !== '' ? ',' : '') . "{$fieldname} AS v{$valuecount}";
         }
         $valuecount += 1;
     }
     if (!$isSubject) {
         // Apply sorting/string matching; only with given property
         $where .= $this->store->getSQLConditions($requestOptions, $valueField, $labelField, $where !== '');
     } else {
         $valueField = '';
     }
     // ***  Now execute the query and read the results  ***//
     $res = $db->select($from, $select, $where, __METHOD__, $usedistinct ? $this->store->getSQLOptions($requestOptions, $valueField) + array('DISTINCT') : $this->store->getSQLOptions($requestOptions, $valueField));
     foreach ($res as $row) {
         $valueHash = '';
         if ($isSubject) {
             // use joined or predefined property name
             $propertykey = $propTable->isFixedPropertyTable() ? $propTable->getFixedProperty() : $row->prop;
             $valueHash = $propertykey;
         }
         // Use enclosing array only for results with many values:
         if ($valuecount > 1) {
             $valuekeys = array();
             for ($i = 0; $i < $valuecount; $i += 1) {
                 // read the value fields from the current row
                 $fieldname = "v{$i}";
                 $valuekeys[] = $row->{$fieldname};
             }
         } else {
             $valuekeys = $row->v0;
         }
         // #Issue 615
         // If the iw field contains a redirect marker then remove it
         if (isset($valuekeys[2]) && $valuekeys[2] === SMW_SQL3_SMWREDIIW) {
             $valuekeys[2] = '';
         }
         // The valueHash prevents from inserting duplicate entries of the same content
         $valueHash = $valuecount > 1 ? md5($valueHash . implode('#', $valuekeys)) : md5($valueHash . $valuekeys);
         // Filter out any accidentally retrieved internal things (interwiki starts with ":"):
         if ($valuecount < 3 || implode('', $fields) != 'p' || $valuekeys[2] === '' || $valuekeys[2][0] != ':') {
             if (isset($result[$valueHash])) {
                 $this->store->getLogger()->log(__METHOD__, "found duplicate for {$propertykey} " . (is_array($valuekeys) ? implode(',', $valuekeys) : $valuekeys));
             }
             $result[$valueHash] = $isSubject ? array($propertykey, $valuekeys) : $valuekeys;
         }
     }
     $db->freeResult($res);
     return $result;
 }
 /**
  * Using a preprocessed internal query description referenced by $rootid,
  * compute the proper result instance output for the given query.
  * @todo The SQL standard requires us to select all fields by which we sort, leading
  * to wrong results regarding the given limit: the user expects limit to be applied to
  * the number of distinct pages, but we can use DISTINCT only to whole rows. Thus, if
  * rows contain sortfields, then pages with multiple values for that field are distinct
  * and appear multiple times in the result. Filtering duplicates in post processing
  * would still allow such duplicates to push aside wanted values, leading to less than
  * "limit" results although there would have been "limit" really distinct results. For
  * this reason, we select sortfields only for POSTGRES. MySQL is able to perform what
  * we want here. It would be nice if we could eliminate the bug in POSTGRES as well.
  *
  * @param Query $query
  * @param integer $rootid
  *
  * @return QueryResult
  */
 private function getInstanceQueryResult(Query $query, $rootid)
 {
     global $wgDBtype;
     $db = $this->store->getConnection();
     $qobj = $this->querySegments[$rootid];
     if ($qobj->joinfield === '') {
         // empty result, no query needed
         $result = new QueryResult($query->getDescription()->getPrintrequests(), $query, array(), $this->store, false);
         return $result;
     }
     $sql_options = $this->getSQLOptions($query, $rootid);
     // Selecting those is required in standard SQL (but MySQL does not require it).
     $sortfields = implode($qobj->sortfields, ',');
     $res = $db->select($db->tableName($qobj->joinTable) . " AS {$qobj->alias}" . $qobj->from, "DISTINCT {$qobj->alias}.smw_id AS id,{$qobj->alias}.smw_title AS t,{$qobj->alias}.smw_namespace AS ns,{$qobj->alias}.smw_iw AS iw,{$qobj->alias}.smw_subobject AS so,{$qobj->alias}.smw_sortkey AS sortkey" . ($wgDBtype == 'postgres' ? ($sortfields ? ',' : '') . $sortfields : ''), $qobj->where, __METHOD__, $sql_options);
     $qr = array();
     $count = 0;
     // the number of fetched results ( != number of valid results in array $qr)
     $missedCount = 0;
     $dataItemCache = array();
     $logToTable = array();
     $hasFurtherResults = false;
     $prs = $query->getDescription()->getPrintrequests();
     $diHandler = $this->store->getDataItemHandlerForDIType(DataItem::TYPE_WIKIPAGE);
     while ($count < $query->getLimit() && ($row = $db->fetchObject($res))) {
         if ($row->iw === '' || $row->iw[0] != ':') {
             // Catch exception for non-existing predefined properties that
             // still registered within non-updated pages (@see bug 48711)
             try {
                 $dataItem = $diHandler->dataItemFromDBKeys(array($row->t, intval($row->ns), $row->iw, '', $row->so));
             } catch (InvalidPredefinedPropertyException $e) {
                 $logToTable[$row->t] = "issue creating a {$row->t} dataitem from a database row";
                 $this->store->getLogger()->log(__METHOD__, $e->getMessage());
                 $dataItem = '';
             }
             if ($dataItem instanceof DIWikiPage && !isset($dataItemCache[$dataItem->getHash()])) {
                 $count++;
                 $dataItemCache[$dataItem->getHash()] = true;
                 $qr[] = $dataItem;
                 // These IDs are usually needed for displaying the page (esp. if more property values are displayed):
                 $this->store->smwIds->setCache($row->t, $row->ns, $row->iw, $row->so, $row->id, $row->sortkey);
             } else {
                 $missedCount++;
                 $logToTable[$row->t] = "skip result for {$row->t} existing cache entry / query " . $query->getHash();
             }
         } else {
             $missedCount++;
             $logToTable[$row->t] = "skip result for {$row->t} due to an internal `{$row->iw}` pointer / query " . $query->getHash();
         }
     }
     if ($db->fetchObject($res)) {
         $count++;
     }
     if ($logToTable !== array()) {
         foreach ($logToTable as $key => $entry) {
             $this->store->getLogger()->logToTable('sqlstore-query-execution', 'query performer', $key, $entry);
         }
     }
     if ($count > $query->getLimit() || $count + $missedCount > $query->getLimit()) {
         $hasFurtherResults = true;
     }
     $db->freeResult($res);
     $result = new QueryResult($prs, $query, $qr, $this->store, $hasFurtherResults);
     return $result;
 }