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); }