protected function findTopicListHistory(array $queries, array $options = array())
 {
     $queries = $this->preprocessSqlArray(reset($queries));
     $res = $this->dbFactory->getDB(DB_SLAVE)->select(array('flow_topic_list', 'flow_tree_node', 'flow_tree_revision', 'flow_revision'), array('*'), array('topic_id = tree_ancestor_id', 'tree_descendant_id = tree_rev_descendant_id', 'tree_rev_id = rev_id') + $queries, __METHOD__, $options);
     $retval = array();
     if ($res) {
         foreach ($res as $row) {
             $row = UUID::convertUUIDs((array) $row, 'alphadecimal');
             $retval[$row['rev_id']] = $row;
         }
     }
     return $retval;
 }
 /**
  * Tests that a PostRevision::fromStorageRow & ::toStorageRow roundtrip
  * returns the same DB data.
  */
 public function testRoundtrip()
 {
     $row = $this->generateRow();
     $object = PostRevision::fromStorageRow($row);
     // toStorageRow will add a bogus column 'rev_content_url' - that's ok.
     // It'll be caught in code to distinguish between external content and
     // content to be saved in rev_content, and, before inserting into DB,
     // it'll be unset. We'll ignore this column here.
     $roundtripRow = PostRevision::toStorageRow($object);
     unset($roundtripRow['rev_content_url']);
     // Due to our desire to store alphadecimal values in cache and binary values on
     // disk we need to perform uuid conversion before comparing
     $roundtripRow = UUID::convertUUIDs($roundtripRow, 'binary');
     $this->assertEquals($row, $roundtripRow);
 }
 /**
  * At the moment, does three things:
  * 1. Finds UUID objects and returns their database representation.
  * 2. Checks for unarmoured raw SQL and errors out if it exists.
  * 3. Finds armoured raw SQL and expands it out.
  *
  * @param array $data Query conditions for DatabaseBase::select
  * @return array query conditions escaped for use
  * @throws DataModelException
  */
 protected function preprocessSqlArray(array $data)
 {
     // Assuming that all databases have the same escaping settings.
     $db = $this->dbFactory->getDB(DB_SLAVE);
     $data = UUID::convertUUIDs($data, 'binary');
     foreach ($data as $key => $value) {
         if ($value instanceof RawSql) {
             $data[$key] = $value->getSql($db);
         } elseif (is_numeric($key)) {
             throw new DataModelException("Unescaped raw SQL found in " . __METHOD__, 'process-data');
         } elseif (!preg_match('/^[A-Za-z0-9\\._]+$/', $key)) {
             throw new DataModelException("Dangerous SQL field name '{$key}' found in " . __METHOD__, 'process-data');
         }
     }
     return $data;
 }
 /**
  * Query topic list ordered by last updated field.  The sort field is in a
  * different table so we need to overwrite parent find() method slightly to
  * achieve this goal
  */
 public function find(array $attributes, array $options = array())
 {
     $attributes = $this->preprocessSqlArray($attributes);
     if (!$this->validateOptions($options)) {
         throw new \MWException("Validation error in database options");
     }
     $res = $this->dbFactory->getDB(DB_MASTER)->select(array($this->table, 'flow_workflow'), 'topic_list_id, topic_id, workflow_last_update_timestamp', array_merge($attributes, array('topic_id = workflow_id')), __METHOD__ . " ({$this->table})", $options);
     if (!$res) {
         // TODO: This should probably not silently fail on database errors.
         return null;
     }
     $result = array();
     foreach ($res as $row) {
         $result[] = UUID::convertUUIDs((array) $row, 'alphadecimal');
     }
     return $result;
 }
 /**
  * {@inheritDoc}
  */
 public function findMulti(array $queries, array $options = array())
 {
     if (!$queries) {
         return array();
     }
     // get cache keys for all queries
     $cacheKeys = $this->getCacheKeys($queries);
     // retrieve from cache (only query duplicate queries once)
     // $fromCache will be an array containing compacted results as value and
     // cache keys as key
     $fromCache = $this->cache->getMulti(array_unique($cacheKeys));
     // figure out what queries were resolved in cache
     // $keysFromCache will be an array where values are cache keys and keys
     // are the same index as their corresponding $queries
     $keysFromCache = array_intersect($cacheKeys, array_keys($fromCache));
     // filter out all queries that have been resolved from cache and fetch
     // them from storage
     // $fromStorage will be an array containing (expanded) results as value
     // and indexes matching $query as key
     $storageQueries = array_diff_key($queries, $keysFromCache);
     $fromStorage = array();
     if ($storageQueries) {
         $fromStorage = $this->backingStoreFindMulti($storageQueries);
         // store the data we've just retrieved to cache
         foreach ($fromStorage as $index => $rows) {
             // backing store returns data that may not be valid to cache (e.g.
             // if we couldn't retrieve content from ExternalStore, we shouldn't
             // cache that result)
             $rows = array_filter($rows, array($this->storage, 'validate'));
             if ($rows !== $fromStorage[$index]) {
                 continue;
             }
             $compacted = $this->rowCompactor->compactRows($rows);
             $callback = function (\BagOStuff $cache, $key, $value) use($compacted) {
                 if ($value !== false) {
                     // somehow, the data was already cached in the meantime
                     return false;
                 }
                 return $compacted;
             };
             $this->cache->merge($cacheKeys[$index], $callback);
         }
     }
     $results = $fromStorage;
     // $queries may have had duplicates that we've ignored to minimize
     // cache requests - now re-duplicate values from cache & match the
     // results against their respective original keys in $queries
     foreach ($keysFromCache as $index => $cacheKey) {
         $results[$index] = $fromCache[$cacheKey];
     }
     // now that we have all data, both from cache & backing storage, filter
     // out all data we don't need
     $results = $this->filterResults($results, $options);
     // if we have no data from cache, there's nothing left - quit early
     if (!$fromCache) {
         return $results;
     }
     // because we may have combined data from 2 different sources, chances
     // are the order of the data is no longer in sync with the order
     // $queries were in - fix that by replacing $queries values with
     // the corresponding $results value
     // note that there may be missing results, hence the intersect ;)
     $order = array_intersect_key($queries, $results);
     $results = array_replace($order, $results);
     $keyToQuery = array();
     foreach ($keysFromCache as $index => $key) {
         // all redundant data has been stripped, now expand all cache values
         // (we're only doing this now to avoid expanding redundant data)
         $fromCache[$key] = $results[$index];
         // to expand rows, we'll need the $query info mapped to the cache
         // key instead of the $query index
         if (!isset($keyToQuery[$key])) {
             $keyToQuery[$key] = $queries[$index];
             $keyToQuery[$key] = UUID::convertUUIDs($keyToQuery[$key], 'alphadecimal');
         }
     }
     // expand and replace the stubs in $results with complete data
     $fromCache = $this->rowCompactor->expandCacheResult($fromCache, $keyToQuery);
     foreach ($keysFromCache as $index => $cacheKey) {
         $results[$index] = $fromCache[$cacheKey];
     }
     return $results;
 }
 /**
  * @param array $primaryKey
  * @return object|null
  * @throws InvalidArgumentException
  */
 public function get(array $primaryKey)
 {
     $primaryKey = UUID::convertUUIDs($primaryKey, 'alphadecimal');
     ksort($primaryKey);
     if (array_keys($primaryKey) !== $this->primaryKey) {
         throw new InvalidArgumentException();
     }
     try {
         return $this->loaded[$primaryKey];
     } catch (OutOfBoundsException $e) {
         return null;
     }
 }
 /**
  * @param UUID[] $nodes
  * @return UUID[]
  * @throws \Flow\Exception\DataModelException
  */
 public function fetchParentMapFromDb(array $nodes)
 {
     // Find out who the parent is for those nodes
     $dbr = $this->dbFactory->getDB(DB_SLAVE);
     $res = $dbr->select($this->tableName, array('tree_ancestor_id', 'tree_descendant_id'), array('tree_descendant_id' => UUID::convertUUIDs($nodes), 'tree_depth' => 1), __METHOD__);
     if (!$res) {
         return array();
     }
     $result = array();
     foreach ($res as $node) {
         if (isset($result[$node->tree_descendant_id])) {
             throw new DataModelException('Already have a parent for ' . $node->tree_descendant_id, 'process-data');
         }
         $descendant = UUID::create($node->tree_descendant_id);
         $result[$descendant->getAlphadecimal()] = UUID::create($node->tree_ancestor_id);
     }
     foreach ($nodes as $node) {
         if (!isset($result[$node->getAlphadecimal()])) {
             // $node is a root, it has no parent
             $result[$node->getAlphadecimal()] = null;
         }
     }
     return $result;
 }
 /**
  * All queries are for roots (guaranteed in findMulti), so anything that falls
  * through and has to be queried from storage will actually need to be doing a
  * special condition either joining against flow_tree_node or first collecting the
  * subtree node lists and then doing a big IN condition
  *
  * This isn't a hot path (should be pre-populated into index) but we still don't want
  * horrible performance
  *
  * @param array $queries
  * @return array
  * @throws \Flow\Exception\InvalidInputException
  */
 protected function findDescendantQuery(array $query)
 {
     $roots = array(UUID::create($query['topic_root_id']));
     $nodeList = $this->treeRepository->fetchSubtreeNodeList($roots);
     if ($nodeList === false) {
         // We can't return the existing $retval, that false data would be cached.
         return array();
     }
     /** @var UUID $topicRootId */
     $topicRootId = UUID::create($query['topic_root_id']);
     $nodes = $nodeList[$topicRootId->getAlphadecimal()];
     return array('rev_type_id' => UUID::convertUUIDs($nodes));
 }
 public function findMulti(array $queries, array $options = array())
 {
     $keys = array_keys(reset($queries));
     $pks = $this->getPrimaryKeyColumns();
     if (count($keys) !== count($pks) || array_diff($keys, $pks)) {
         return $this->fallbackFindMulti($queries, $options);
     }
     $conds = array();
     $dbr = $this->dbFactory->getDB(DB_SLAVE);
     foreach ($queries as $query) {
         $conds[] = $dbr->makeList($this->preprocessSqlArray($query), LIST_AND);
     }
     unset($query);
     $conds = $dbr->makeList($conds, LIST_OR);
     $result = array();
     // options can be ignored for primary key search
     $res = $this->find(array(new RawSql($conds)));
     if (!$res) {
         return $result;
     }
     // create temp array with pk value (usually uuid) as key and full db row
     // as value
     $temp = new MultiDimArray();
     foreach ($res as $val) {
         $val = UUID::convertUUIDs($val, 'alphadecimal');
         $temp[ObjectManager::splitFromRow($val, $this->primaryKey)] = $val;
     }
     // build return value by mapping the database rows to the matching array
     // index in $queries
     foreach ($queries as $i => $val) {
         $val = UUID::convertUUIDs($val, 'alphadecimal');
         $pk = ObjectManager::splitFromRow($val, $this->primaryKey);
         $result[$i][] = isset($temp[$pk]) ? $temp[$pk] : null;
     }
     return $result;
 }
 /**
  * @param ResultDuplicator $duplicator
  * @param array $revisionIds Binary strings representing revision uuid's
  * @return array
  * @throws DataModelException
  */
 protected function findRevIdReal(ResultDuplicator $duplicator, array $revisionIds)
 {
     if ($revisionIds) {
         //  SELECT * from flow_revision
         //	  JOIN flow_tree_revision ON tree_rev_id = rev_id
         //   WHERE rev_id IN (...)
         $dbr = $this->dbFactory->getDB(DB_MASTER);
         $tables = array('flow_revision');
         $joins = array();
         if ($this->joinTable()) {
             $tables['rev'] = $this->joinTable();
             $joins = array('rev' => array('JOIN', "rev_id = " . $this->joinField()));
         }
         $res = $dbr->select($tables, '*', array('rev_id' => $revisionIds), __METHOD__, array(), $joins);
         if (!$res) {
             // TODO: dont fail, but dont end up caching bad result either
             throw new DataModelException('query failure', 'process-data');
         }
         foreach ($res as $row) {
             $row = UUID::convertUUIDs((array) $row, 'alphadecimal');
             $duplicator->merge($row, array($row));
         }
     }
     return $duplicator->getResult();
 }
 /**
  * Returns a boolean true/false if the getMulti()-operation for the given
  * attributes has already been resolves and doesn't need to query any
  * outside cache/database.
  * Determining if a find() has not yet been resolved may be useful so that
  * additional data may be loaded at once.
  *
  * @param array $objectIds Ids to getMulti()
  * @return bool
  */
 public function gotMulti(array $objectIds)
 {
     if (!$objectIds) {
         return true;
     }
     $primaryKey = $this->storage->getPrimaryKeyColumns();
     $queries = array();
     foreach ($objectIds as $id) {
         $query = array_combine($primaryKey, ObjectManager::makeArray($id));
         $query = UUID::convertUUIDs($query, 'alphadecimal');
         if (!$this->mapper->get($query)) {
             $queries[] = $query;
         }
     }
     if ($queries && $this->mapper instanceof Mapper\CachingObjectMapper) {
         return false;
     }
     return $this->foundMulti($queries);
 }