public function search($options) { // we query the listings table which is sharded by month. // because it is sharded by month, we have to do a few more tricks // to get the data-set we want. // first, we try to narrow it down as much as possible by the criteria specified. $clauses = array(); // are we looking for auctions that are still in progress or already finished? $clauses[] = $this->db->prep('closed = ?', $options->closed ? 1 : 0); // if we have a specific item id we are looking for, query for that. if (isset($options->item_id)) { $clauses[] = $this->db->prep('item_id IN (%i)', $options->item_id); } // do we know the seller? if (isset($options->seller)) { $clauses[] = $this->db->prep('seller IN (%i)', $options->seller); } // sometimes, rarely we are looking for an auction purchased by a specific buyer. // obviously these auctions are already closed. should i sanity check the closed param? if (isset($options->buyer)) { $clauses[] = $this->db->prep('buyer IN (%i)', $options->buyer); } // we can narrow by the person who is currently the leading bidder. can't search by past // bidders since that is more of a bid history search. // haven't written that yet. if (isset($options->bidder)) { $clauses[] = $this->db->prep('bidder IN (%i)', $options->bidder); } // are we looking for a bid-only auction? if ($options->only == 'bid') { $clauses[] = 'price = 0'; } // how about a buy-now only auction? if ($options->only == 'buy') { $clauses[] = 'step = 0'; } // look for items only above a given price range. if ($options->floor && ctype_digit($options->floor)) { $clauses[] = $this->db->prep('pricesort >= %i', $options->floor); } // look for items only below a given price range. if ($options->ceiling && ctype_digit($options->ceiling)) { $clauses[] = $this->db->prep('pricesort <= %i', $options->ceiling); } // how do we want the result set sorted? $sort = $options->sort; $order = ''; switch ($sort) { case 'low_price': $order = 'pricesort ASC'; break; case 'high_price': $order = 'pricesort DESC'; break; case 'just_added': $order = 'created DESC'; break; case 'expires_soon': $clauses[] = $this->db->prep('expires > %i', Souk\Util::now()); $order = 'expires ASC'; break; case 'expires_soon_delay': $clauses[] = $this->db->prep('expires > %i', Souk\Util::now() + Souk\UTIL::MIN_EXPIRE); $order = 'expires ASC'; break; default: $key = $row['expires'] . '.' . $id; break; } $ds = Souk\Util::dateshard(); // start with the shard a few weeks out from now, which is where the new listings are. $ds->setTimestamp(Souk\Util::now() + Souk\UTIL::MAX_EXPIRE); // if the auction is still active, we don't have to search the really old shards. // stop at the shard for this week. if (!$options->closed) { $ds->setCutoff(1); } // start looping throw the shards and querying. $ids = array(); $where = $clauses ? 'WHERE ' . implode(' AND ', $clauses) : ''; if ($order) { $order = ' ORDER BY ' . $order; } // don't return more rows than the hard search limit imposed by souk. // after more than about 1000 rows, more results become meaningless. who paginates through all of that? // need them to somehow narrow their search more. $limit = 'LIMIT ' . Souk\Util::SEARCH_LIMIT; $ds = Souk\Util::dateshard(); foreach ($ds as $shard) { $table = $this->table($shard); if (\Gaia\Souk\Storage::isAutoSchemaEnabled()) { $this->create($table); } $sql = "SELECT row_id, pricesort, created, expires FROM {$table} {$where} {$order} {$limit}"; //print "\n" . $sql; // run the query $rs = $this->execute($sql); //print_r( $rs ); // pull out all the rows matched by the query. // we are making the key of the id list contain the // value of what we sort by, so we can do a keysort later, and order the // result in php since we have to span many shards. while ($row = $rs->fetch()) { $id = Souk\Util::composeId($shard, $row['row_id']); switch ($sort) { case 'low_price': case 'high_price': $key = $row['pricesort'] . '.' . $id; break; case 'just_added': $key = $row['created'] . '.' . $id; break; case 'expires_soon': case 'expires_soon_delay': default: $key = $row['expires'] . '.' . $id; break; } $ids[$key] = $id; } $rs->free(); } // now that we are all done fetching the rows, sort. switch ($sort) { case 'low_price': case 'expires_soon': case 'expires_soon_delay': ksort($ids, SORT_NUMERIC); break; default: krsort($ids, SORT_NUMERIC); break; } // since we queried many shards we could potentially have quite a few more // ids than the max limit. slice off only the top most rows. // those are the ones we care about. // we only need the other values so we can sort in php across all the shards. // a little bit inefficient, but it is just a list of numbers, and we are gonna // cache it for a long time in the caching layer. return array_values(array_slice($ids, 0, Souk\Util::SEARCH_LIMIT)); }
/** * buy a listing immediately ... * only works with those items that set a price for buy-now * @return Souk\Listing. */ public function buy($id, array $data = NULL) { // create an internal transaction if one hasn't been passed in Transaction::start(); try { // get a row lock on the listing. $listing = $this->get($id, TRUE); // if we didn't get any listing, toss an exception. if (!$listing || !$listing->id) { throw new Exception('not found', $id); } // need the current time to make sure it is a valid time to buy $ts = Souk\Util::now(); // we assume the current user is the buyer $buyer = $this->user(); // if no user id passed into the constructor, can't buy if (!Souk\Util::validatePositiveInteger($buyer)) { throw new Exception('invalid buyer', $buyer); } // can't buy if the listing is already closed if ($listing->closed) { throw new Exception('sold', $listing); } // can't buy if the seller is the same as the buyer. if ($listing->seller == $buyer) { throw new Exception('invalid buyer', $listing); } // can't buy if no price was set. if ($listing->price < 1) { throw new Exception('bid only', $listing); } // cant buy if the listing has expired. if ($listing->expires <= $ts) { throw new Exception('expired', $listing); } // keep a pristine copy of what the listing looks like right now for later. // other wrapper layers can use this to make comparisons to what changed. $listing->setPriorState($listing); $listing->touch = $ts; $listing->closed = 1; $listing->buyer = $buyer; $this->storage()->buy($listing); // if we created the transaction internally, commit it. Transaction::commit(); // done. return $listing; // looks like something went wrong along the way. } catch (Exception $e) { // revert any transaction stuff. // (detach an internal transaction if there is one) Transaction::rollback(); // toss an exception. throw $e; } }
/** * low level call to update a search vector cache key. */ protected function updateSearchVector($key) { $this->cache(self::SEARCH_VECTOR_PREFIX)->set($key, Util::now() . '.' . mt_rand(1, 1000000000), 0); }