From badd37430ff2e73b434882865360bd79de2e8209 Mon Sep 17 00:00:00 2001 From: Brennen Bearnes Date: Fri, 26 Sep 2014 14:51:50 -0600 Subject: [PATCH] SparkFinder & SparkFriendFinder --- lib/SparkFinder.php | 223 +++++++++ lib/SparkFriendFinder.php | 975 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 1198 insertions(+) create mode 100644 lib/SparkFinder.php create mode 100644 lib/SparkFriendFinder.php diff --git a/lib/SparkFinder.php b/lib/SparkFinder.php new file mode 100644 index 0000000..8e3e6ce --- /dev/null +++ b/lib/SparkFinder.php @@ -0,0 +1,223 @@ +getNext()) { + * // Do something with $record_instance + * } + * + * which will (in general) safely run until results are exhausted, or iterate + * over the results using foreach(), like so: + * + * foreach ($things as $id => $record_instance) { + * // Do something with $record_instance + * } + * + * ...but don't do that, since foreach() is quite likely to eat all of your + * available memory if you have very many results, as it will try to build + * the entire array before it ever starts iterating. If you find a way around + * this, I'd love to hear about it. + */ +class SparkFinder extends \SparkLib\Iterator { + + protected $_recordClass; + protected $_table = null; + protected $_tableKey = null; + protected $_params = null; + protected $_resultSet = null; + protected $_query; + + // We'll track the iterator's position and store results with these: + protected $_position = 0; + protected $_row = null; + + /** + * Get a new finder. Takes a string containing the class of dinosaur + * to instantiate, and a query to find the dinosaurs with. Optionally + * takes an array of parameters for bound queries. + */ + public function __construct ($record_class, $querystring = null, $params = null) + { + $this->_recordClass = $record_class; + + /* Careful here - this will usually do the right thing, but class_exists() + invokes the autoloader, which is sometimes dumb enough to go after a + file that doesn't exist: */ + + if (! class_exists($this->_recordClass)) + throw new InvalidArgumentException("No such record class: {$this->_recordClass}"); + + // Pull in some metadata from the record class: + $this->handleClass(); + + // Try for a sane default query, if we know about a table + // (we might not know about a table if we're dealing with + // something other than a child of SparkRecord, like a SparkRecordGeneric. + if (is_null($querystring) && (! is_null($this->_table))) + $querystring = 'SELECT * FROM ' . $this->_table; + + $this->_query = $querystring; + + if (is_array($params)) + $this->_params = $params; + + $this->doQuery(); + } + + /** + * Start over with a fresh run of the query. Here for compatibility with the + * Iterator interface, and works, but if you're actually using it much there's + * a better-than-even chance you need to rethink your code. + */ + public function rewind () + { + // Reset, if we need to: + if ($this->_position !== 0) { + $this->_position = 0; + $this->doQuery(); + } + } + + public function key () { return $this->_row[$this->_tableKey]; } + public function recordClass () { return $this->_recordClass; } + public function tableKey () { return $this->_tableKey; } + public function valid () { return is_array($this->_row); } + public function position() { return $this->_position; } + + /** + * How many rows in the current statement results? + */ + public function count () + { + if (! $this->_resultSet) + return false; + + return $this->_resultSet->rowCount(); + } + + /** + * Advance to the next record. + */ + public function next () + { + if (! isset($this->_resultSet)) + throw new Exception('No current results - something bad happened here.'); + + ++$this->_position; + + // Use the next thing from the resultset: + if ($row = $this->_resultSet->fetch(\PDO::FETCH_ASSOC)) + $this->_row = $row; + else + $this->_row = null; + } + + /** + * Return an object for the current record. + */ + public function current () + { + // Do we have a record array? Return false if not. + if (! $this->valid()) + return false; + + // We've got a record array; make it into a dinosaur. + $class = $this->_recordClass; + return new $class($this->_row); + } + + /** + * Load the next record and return an object for it. + */ + public function getNext () + { + // Will be a record object or false; + $result = $this->current(); + + // If we got an object, we'll advance to the next record. + if (false !== $result) + $this->next(); + + return $result; + } + + /** + * @return string current query + */ + public function getQuery () + { + return $this->_query; + } + + /** + * Set up some useful local state based on the dinosaur/record class + * we're working with. + */ + protected function handleClass () + { + // Hack our way around some PHP fail: + $rc = $this->_recordClass; + + // If these methods exist, we're probably dealing with a SparkRecord. + // Otherwise, it's something else. + if (method_exists($rc, 'getTableKey')) { + $this->_tableKey = $rc::getDefaultTableKey(); + $this->_table = $rc::getDefaultTableName(); + $this->_tableRecord = $rc::getDefaults(); + } + } + + /** + * Talk to our friend, the database, and stash results appropriately. + */ + protected function doQuery () + { + if (constant('DB_LOGQUERIES')) { + \SparkLib\Fail::log($this->_query); + if (is_array($this->_params)) { + \SparkLib\Fail::log($this->_params); + } + } + + try { + // Get a resultset and stash it: + + if (is_array($this->_params)) { + $this->_resultSet = $this->getDBH()->prepare($this->_query); + $this->_resultSet->execute($this->_params); + } else { + // If we have no parameters, there's no real purpose to prepping + // the query, so we may as well spare ourselves the performance hit: + $this->_resultSet = $this->getDBH()->query($this->_query); + } + + } catch (\PDOException $e) { + $param_str = ''; + if (isset($this->_params)) { + $param_str = "Params: " . implode(', ', $this->_params) . "\n"; + } + $message = 'Query failed in SparkFinder: ' . $e->getMessage() . "\n" . $param_str . "\nQuery: " . $this->_query; + throw new Exception($message); + } + + // Pre-load our first result: + $this->_row = $this->_resultSet->fetch(\PDO::FETCH_ASSOC); + } + + protected function getDBH () + { + return DB::getInstance(); + } + +} diff --git a/lib/SparkFriendFinder.php b/lib/SparkFriendFinder.php new file mode 100644 index 0000000..98b0da8 --- /dev/null +++ b/lib/SparkFriendFinder.php @@ -0,0 +1,975 @@ +Orders->orderBy('date_purchased', 'DESC')->limit(5)->find(); + * while($order = $orders->getNext()) + * // whatever + * + * It should be noted that while a foreach() on the return of find() will + * also work, it loads everything into memory first and is thus a lot more + * costly than the above while() example, as that iterates over one result at + * a time. So, use while(). + * + * EXAMPLE 2: Find a single storefront product by SKU: + * + * $product = StorefrontProductSaurus::finder() + * ->where('products_model')->eq('DEV-00666') + * ->find()->current(); + * + * This uses the static finder() method available to all generated records. + * So, find() returns a SparkFriendFinder with the first result (the only + * one, in this case) preloaded, and current() returns it. + */ +class SparkFriendFinder extends \SparkLib\Iterator { + + const NULLS_LAST = 'LAST'; + const NULLS_FIRST = 'FIRST'; + + protected $_recordClass; + protected $_relationships; + + // A class to instantiate with the record instance as its only + // parameter and return + protected $_wrapperClass = null; + + // Table and primary key of the record class we're searching for. + protected $_table = null; + protected $_tableKey = null; + protected $_tableRecord = null; + + protected $_sparkfinder = null; + protected $_total = null; + + // Initial SELECT and WHERE. + protected $_default_query = ''; + // Full query with constraints. + protected $_query = ''; + + // Those constraints: + protected $_distinctOn = ''; + protected $_join = ''; + protected $_with = ''; + protected $_where = ''; + protected $_orderBy = ''; + protected $_groupBy = ''; + protected $_limit = ''; + protected $_forUpdate = ''; + + // For the time being these two will only get set in child classes of this one. + protected $_left_join = ''; + protected $_extraselects = ''; + + protected $_key; + protected $_id; + + protected $_buffered = true; + + protected $_selectIdOnly = false; + + /** + * Get a new finder. Takes a string containing the classname of dinosaur + * to instantiate, and an optional key and id. + * + * If a key and id are passed, we are looking for records in the context of + * another one, e.g. getting a customer's orders. Otherwise, we are + * (probably) using the static SomeRecord::finder() method. + */ + public function __construct ($record_class, $key = null, $id = null) + { + $this->_recordClass = $record_class; + $this->_key = $key; + $this->_id = $id; + + // It's worth noting that class_exists() probably invokes the Autoloader here + if (! class_exists($this->_recordClass)) + throw new InvalidArgumentException("No such record class: {$this->_recordClass}"); + + // Pull in some metadata from the record class: + $this->handleClass(); + } + + /** + * Re-run the previous query, starting over from the first record. + */ + public function rewind () + { + if (! $this->_sparkfinder) + return false; + $this->getResults(); + } + + // Simple getters + public function key () { return $this->current()->getId(); } + public function recordClass () { return $this->_recordClass; } + public function tableKey () { return $this->_tableKey; } + public function tableRecord () { return $this->_tableRecord; } + + /** + * Return current position within the resultset. + */ + public function position() + { + if (isset($this->_sparkfinder)) + return $this->_sparkfinder->position(); + else + return 0; + } + + /** + * Check if there's a currently available record. + * + * @return bool - yay or nay + */ + public function valid () + { + return $this->_sparkfinder->valid(); + } + + /** + * How many rows in the current statement results? + */ + public function count () + { + if (! $this->_sparkfinder) + return false; + return $this->_sparkfinder->count(); + } + + /** + * How many rows total with these params? + * + * Keeps the joins, as they may affect the total; drops the _with because + * that's just overhead. + */ + public function total () + { + $dbi = SparkDBI::getInstance(); + + if($this->_total) + return $this->_total; + + $q = 'SELECT COUNT(' . $this->_table . '.' . $this->_tableKey . ') AS total ' . + ' FROM ' . $this->_table . + $this->_join . + $this->_left_join . + ' WHERE true ' . + $this->_where; + + $totalArray = $dbi->fetchRow($q); + $this->_total = (int)$totalArray['total']; + + return $this->_total; + } + + /** + * How many rows total with these params? + * + * use this if you have a group by statement in your query + */ + public function group_total () + { + $dbi = SparkDBI::getInstance(); + + if ($this->_total) + return $this->_total; + + $q = 'SELECT COUNT(0) AS total FROM (SELECT ' . $this->_table . '.' . $this->_tableKey . + ' FROM "' . $this->_table . '"' . + $this->_join . + $this->_left_join . + ' WHERE true ' . + $this->_where . + $this->_groupBy . + ') g'; + + $totalArray = $dbi->fetchRow($q); + $this->_total = $totalArray['total']; + + return $this->_total; + } + + /** + * Advance to the next record. + */ + public function next () + { + if (! $this->_sparkfinder) + throw new Exception('No current results - find() not called?'); + return $this->_sparkfinder->next(); + } + + /** + * Return an object for the current record. + * + * @return SomeSaurus - a $this->_recordClass + */ + public function current () + { + if (! $this->valid()) + return false; + + if ($this->_wrapperClass) { + $wrapper = $this->_wrapperClass; + // yo dawg, I heard you like classes + return new $wrapper($this->_sparkfinder->current()); + } + + return $this->_sparkfinder->current(); + } + + /** + * Load the next record and return an object for it. + * + * @return SomeSaurus - a $this->_recordClass + */ + public function getNext () + { + // Assume we want to go ahead and find() if there aren't + // already results to get + if (! isset($this->_sparkfinder)) + $this->find(); + + // Will be a record object or false; + $result = $this->current(); + + // If we got an object, we'll advance to the next record. + if (false !== $result) + $this->next(); + + return $result; + } + + /** + * @return string current query + */ + public function getQuery () + { + return $this->_query; + } + + /** + * @return true if there are more available. + */ + public function hasNext () + { + return $this->count() && ($this->_sparkfinder->position() < $this->count()); + } + + /** + * Load some class properties with meta data about $this->_recordClass. + */ + protected function handleClass () + { + // Hack our way around some PHP fail: + $rc = $this->_recordClass; + + // If these methods exist, we're probably dealing with a SparkRecord. + // Otherwise, it's something else. + if (method_exists($rc, 'getTableKey')) { + $this->_tableKey = $rc::getDefaultTableKey(); + $this->_table = $rc::getDefaultTableName(); + $this->_tableRecord = $rc::getDefaults(); + } + } + + /** + * Set a wrapper class for results. + */ + public function wrap ($class) + { + if (class_exists($class)) { + $this->_wrapperClass = $class; + } else { + throw new Exception($class . ' does not exist'); + } + return $this; + } + + /** + * Build the query. + * + * @return string sql + */ + public function buildQuery () + { + // Build and save the query + $this->_query = $this->_base_query() . + $this->_where . + $this->_groupBy . + $this->_orderBy . + $this->_limit . + $this->_forUpdate . "\n"; + + return $this->_query; + } + + /** + * Build the query, execute it, and load the first object. + * + * @return SparkFriendFinder - $this, for method chains. + */ + public function find () + { + // Build and save the query + $this->buildQuery(); + + // clear query parts + $this->_join = ''; + $this->_where = ''; + $this->_orderBy = ''; + $this->_limit = ''; + $this->_with = ''; + + // Get a SparkFinder and stash it + $this->getResults(); + + return $this; + } + + /** + * Get a database handle. + */ + protected function getDBH () + { + return DB::getInstance(); + } + + /** + * Turn the current query into a result set and stash it. + */ + protected function getResults () + { + try { + $this->_sparkfinder = new SparkFinder( + $this->_recordClass, + $this->_query + ); + } catch (Exception $e) { + throw new Exception('Query fail: ' . $this->_query . ' -- ' . $e->getMessage()); + } + + // PDO TODO: Come up with a graceful way to request an unbuffered resultset. + // (Presumably necessitates a connection per resultset.) + } + + /** + * Make the query a FOR UPDATE query. + * + * Essentially what this does is put a row lock on + * every row that is part of the SELECT query such that + * any other query (with a FOR UPDATE clause) that matches + * any of the selected rows for this finder will block + * until the current transaction has completed, either by + * a commit or rollback. + */ + public function forUpdate () { + $this->_forUpdate = ' FOR UPDATE'; + return $this; + } + + /** + * Run find() and get the first result. Just a utility to shorten + * things up a bit. If find() has already been called, will act + * as an alias to current(), rather than re-running a bare query. + * + * @return SparkRecord + */ + public function getOne () + { + if (isset($this->_sparkfinder)) + return $this->current(); + return $this->find()->current(); + } + + /** + * JOIN the given dinosaur by the relationship defined in records.ini + */ + public function with ($dinosaur) + { + $record_class = $this->_recordClass; + $relationships = $record_class::getRelationships(); + + if (! isset($relationships[$dinosaur])) + throw new Exception('Invalid relationship: ' . $dinosaur); + + if(in_array($relationships[$dinosaur]['type'], array('has_one', 'has_many'))) { + $our_key = $relationships[$dinosaur]['pk']; + $their_key = $relationships[$dinosaur]['fk']; + } elseif($relationships[$dinosaur]['type'] == 'belongs_to') { + $our_key = $relationships[$dinosaur]['fk']; + $their_key = $relationships[$dinosaur]['pk']; + } + + // If this is a one to many relationship, use a left join + /* + $has_many = $record_class::getHasManyDefaults(); + if($has_many[$dinosaur]) + $this->_join .= ' LEFT'; + */ + + $classname = $relationships[$dinosaur]['class']; + $this->_with .= $this->_generate_with($classname); + $this->_join .= " JOIN {$classname::getDefaultTableName()} {$classname::getDefaultTableName()} ON {$this->_table}.{$our_key} = {$classname::getDefaultTableName()}.{$their_key} \n"; + + return $this; + } + + /** + * JOIN another dinosaur by the relationship + * defined in records.ini + * TODO: Maybe merge this with with() or something less verbose? + */ + public function withOn ($dinosaur, $joinon) + { + $record_class = $joinon . 'Saurus'; + $relationships = $record_class::getRelationships(); + + if(! isset($relationships[$dinosaur])) + throw new Exception('Invalid relationship: ' . $dinosaur . ' on ' . $record_class); + + if(in_array($relationships[$dinosaur]['type'], array('has_one', 'has_many'))) { + $our_key = $relationships[$dinosaur]['pk']; + $their_key = $relationships[$dinosaur]['fk']; + } elseif($relationships[$dinosaur]['type'] == 'belongs_to') { + $our_key = $relationships[$dinosaur]['fk']; + $their_key = $relationships[$dinosaur]['pk']; + } + + $classname = $relationships[$dinosaur]['class']; + $this->_with .= $this->_generate_with($classname); + $this->_join .= " JOIN {$classname::getDefaultTableName()} {$classname::getDefaultTableName()} ON {$record_class::getDefaultTableName()}.{$our_key} = {$classname::getDefaultTableName()}.{$their_key} \n"; + + return $this; + } + + /** + * LEFT JOIN a table + * + * @param string $sql LEFT JOIN table on pkey = fkey + */ + public function leftJoin ($sql) + { + $sql = trim($sql); + + if (! strlen($sql)) + return $this; + + // make sure it is left join sql + if (strtoupper(substr($sql, 0, 9)) != 'LEFT JOIN') + throw new Exception('Invalid LEFT JOIN clause: ' . $sql); + + $this->_left_join = ' ' . $sql . ' '; + return $this; + } + + /** + * put some extra fields in the SELECT + * for use with leftJoin() or sql functions + * + * BEWARE! If you use this in public-facing code with user inputs, + * Brennen WILL murder you. + * + * @param array $fields fields or sql expressions like COUNT(*) + */ + public function extraSelects ($fields) + { + if (! is_array($fields) || ! count($fields)) + throw new Exception('extraSelects expects a non-empty array'); + + $sql = implode(', ', $fields) . ','; // needs trailing comma + + $this->_extraselects = $sql . ' '; + return $this; + } + + /** + * To select only the primary table id and any extra selects + * (instead of select * from all tables) + * + */ + public function selectIdOnly () + { + $this->_selectIdOnly = true; + return $this; + } + + /** + * FIELD table field name + * **BEWARE: This function is rendered useless with a join or extraselects. + */ + public function isValidField ($field) + { + if ($this->_join === '' && $this->_left_join === '' && $this->_extraselects === '') { + return isset($this->_tableRecord[$field]); + } else { + return true; + } + } + + /** + * Add to the WHERE clause for a given field. + * + * Be careful - this may fail if you're doing a ->with() _after_ the where(). + * Do with() first. + * + * @param $field string + * @return SparkFriendFinder + */ + public function where ($field) + { + if (!$this->isValidField($field)) + throw new Exception('Invalid field in where clause: ' . $field); + + $this->_where .= "\n AND " . $field; + return $this; + } + + public function whereSubstring($field, $from, $for) + { + if (!$this->isValidField($field)) + throw new Exception('Invalid field in where clause: ' . $field); + + if (!is_integer($from) || !is_integer($for)) + throw new Exception('Invalid from/for in whereSubstring clause ' . $field); + + $this->_where .= "\n AND substring(" . $field . " from " . $from . " for " . $for . ")"; + return $this; + } + + /** + * Add a WHERE lower($field) + * + * @param $field string + * @return SparkFriendFinder + */ + public function whereLower ($field) + { + if (!$this->isValidField($field)) + throw new Exception('Invalid field in where clause: ' . $field); + + $this->_where .= "\n AND lower(" . $field . ")"; + return $this; + } + + public function orWhere ($field) + { + if (!$this->isValidField($field)) + throw new Exception('Invalid field in where clause: ' . $field); + + $this->_where .= "\n OR " . $field; + return $this; + } + + public function orWhereSubstring($field, $from, $for) + { + if (!$this->isValidField($field)) + throw new Exception('Invalid field in where clause: ' . $field); + + if (!is_integer($from) || !is_integer($for)) + throw new Exception('Invalid from/for in orWhereSubstring clause ' . $field); + + $this->_where .= "\n OR substring(" . $field . " from " . $from . " for " . $for . ")"; + return $this; + } + + /** + * Add to the WHERE clause for a given subquery evaluation. + * + * Like with where() be careful - this may fail if you're doing + * a ->with() _after_ the where(). Do with() first. + * + * @param $subquery string + * @return SparkFriendFinder + */ + public function subquery ($subquery) + { + $this->_where .= "\n AND (\n" + . $subquery . "\n" + . ")"; + + return $this; + } + + /** + * startSub - begin a sub-where clause in the query. + * + * example: + * + * $orders = OrderSaurus::finder() + * $orders->startSub(); + * $orders->orWhere('customers_name')->like('%' . $s_query . '%'); + * $orders->orWhere('customers_email_address')->like('%' . $s_query . '%'); + * $orders->endSub(); + * $orders->find(); + * + * @param $op string of either AND or OR to prepend to the outside of the subclause + * @param $subop string of either AND or OR which will affect the subclause + */ + public function startSub ($op = 'AND', $subop = 'OR') + { + if('AND' == strtoupper($op)) + $this->_where .= "\n AND ( "; + if('OR' == strtoupper($op)) + $this->_where .= "\n OR ( "; + + if('AND' == strtoupper($subop)) + $this->_where .= ' true '; + if('OR' == strtoupper($subop)) + $this->_where .= ' false '; + + return $this; + } + + public function endSub () + { + $this->_where .= "\n ) "; + return $this; + } + + public function orderBy ($field, $direction = 'ASC', $nulls = null) + { + if ('ASC' != $direction && 'DESC' != $direction) + throw new Exception('Invalid direction in "orderBy" clause: ' . $direction); + + if (isset($nulls) && (('FIRST' !== $nulls) && ('LAST' !== $nulls))) + throw new Exception('Invalid NULLS in "orderBy" clause: ' . $nulls); + + if ($field instanceof \SparkLib\DB\Literal) { + // right now, this just handles Random() - might be other things, tho: + $field = $field->literal(); + } else { + // it's a string - check whether it's safe to order by + // We're unable to validate fields if a FIND_IN_SET or COALESCE is present. + if (!stristr($field, 'FIND_IN_SET') && !stristr($field, 'COALESCE')) { + if (!$this->isValidField($field)) + throw new Exception('Invalid field in orderBy clause: ' . $field); + } + } + + if ('' == $this->_orderBy) + $this->_orderBy = ' ORDER BY ' . $field . ' ' . $direction; + else + $this->_orderBy .= ', ' . $field . ' ' . $direction; + + if (isset($nulls)) + $this->_orderBy .= ' NULLS ' . $nulls; + + return $this; + } + + public function groupBy ($field) + { + // We're unable to validate fields if a FIND_IN_SET is present. + if (!stristr($field, 'FIND_IN_SET')) { + if (!$this->isValidField($field) && $field !== self::RAND) + throw new Exception('Invalid field in "groupBy" clause: ' . $field); + } + + if ('' == $this->_groupBy) + $this->_groupBy = ' GROUP BY ' . $field; + else + $this->_groupBy .= ', ' . $field; + + return $this; + } + + public function distinctOn ($field) + { + // We're unable to validate fields if a FIND_IN_SET is present. + if (!stristr($field, 'FIND_IN_SET')) { + if (!$this->isValidField($field) && $field !== self::RAND) + throw new Exception('Invalid field in "distinct" clause: ' . $field); + } + + if ('' == $this->_distinctOn) + $this->_distinctOn = ' DISTINCT ON (' . $field . ')'; + else + $this->_distinctOn = str_replace(')', ',', $this->_distinctOn) . $field . ')'; + + return $this; + + } + + /** + * PDO TODO: document + */ + public function limit ($limit, $offset = 0) + { + $limit = (int) $limit; + $offset = (int) $offset; + + if ('' != $this->_limit) + throw new Exception('You may only impose one "limit" clause.'); + + if($limit > 0) + $this->_limit = ' LIMIT ' . $limit; + + $this->_limit .= ' OFFSET ' . $offset; + + return $this; + } + + /** + * PDO TODO: document + */ + public function eq ($val) + { + if (is_array($val)) { + $this->_where .= ' IN('; + foreach($val as $i => $v) { + $this->_where .= $this->render($v); + if($i+1 < count($val)) + $this->_where .= ','; + } + $this->_where .= ')'; + } else { + $this->_where .= ' = ' . $this->render($val); + } + return $this; + } + + /** + * A hack to look for the lower-case version of a string. + * + * We'll let the database handle lower-casing, since there's a good + * chance it's smarter than PHP. + */ + public function eqLower ($val) + { + if (! is_string($val)) { + throw new \Exception('lowerEq() expects a string, for now'); + } + + $this->_where .= ' = lower(' . $this->render($val) . ')'; + return $this; + } + + /** + * PDO TODO: document + */ + public function ne ($val) + { + if (is_array($val)) { + $this->_where .= ' NOT IN('; + foreach($val as $i => $v) { + $this->_where .= $this->render($v); + if($i+1 < count($val)) + $this->_where .= ','; + } + $this->_where .= ')'; + } else { + $this->_where .= ' <> ' . $this->render($val); + } + return $this; + } + + /** + * IS - use with a DB\Null, for example. + */ + public function is ($val) + { + $this->_where .= ' IS ' . $this->render($val); + return $this; + } + + /** + * IS NOT - use with a DB\Null, for example. + */ + public function isnot ($val) + { + $this->_where .= ' IS NOT ' . $this->render($val); + return $this; + } + + /** + * IN() + * Added so we can use sub-selects. NOT ESCAPED. + * eq/ne should be used under normal circumstances + */ + public function in ($val) + { + // this isn't escaped because we need IN((query)) to be quote-free. + // be seriously fucking careful. + $this->_where .= ' IN(' . $val . ')'; + return $this; + } + + /** + * like in, but not. NOT ESCAPED. + */ + public function nin ($val) + { + // this isn't escaped because we need NOT IN((query)) to be quote-free. + // be really goddamned careful. + $this->_where .= ' NOT IN(' . $val . ')'; + return $this; + } + + public function gt ($val) { $this->_where .= ' > ' . $this->render($val); return $this; } + + public function lt ($val) { $this->_where .= ' < ' . $this->render($val); return $this; } + + public function between($val1, $val2) { $this->_where .= ' BETWEEN ' . $this->render($val1) . ' AND ' . $this->render($val2); return $this; } + + public function gte ($val) { $this->_where .= ' >= ' . $this->render($val); return $this; } + + public function lte ($val) { $this->_where .= ' <= ' . $this->render($val); return $this; } + + public function like ($val) { $this->_where .= ' LIKE ' . $this->render($val); return $this; } + + public function plus ($val) { $this->_where .= ' + ' . $this->render($val); return $this; } + + public function minus ($val) { $this->_where .= ' - ' . $this->render($val); return $this; } + + /** + * Case-insensitive like. + */ + public function ilike ($val) + { + // MySQL doesn't support ILIKE + if ('mysql' !== constant('\DB_SERVER_TYPE')) { + $this->_where .= ' ILIKE ' . $this->render($val); + return $this; + } + return $this->like($val); + } + + public function has($val) { + return $this->like('%' . $val . '%'); + } + + public function ihas($val) { + return $this->ilike('%' . $val . '%'); + } + + public function nlike ($val) { $this->_where .= ' NOT LIKE ' . $this->render($val); return $this; } + + /** + * Case-insensitive nlike. + */ + public function nilike ($val) + { + // MySQL doesn't support ILIKE + if ('mysql' !== constant('\DB_SERVER_TYPE')) { + $this->_where .= ' NOT ILIKE ' . $this->render($val); + return $this; + } + return $this->nlike($val); + } + + /** + * Use buffered results. + */ + public function buffered () + { + $this->_buffered = true; + return $this; + } + + /** + * Use unbuffered results. + */ + public function unbuffered () + { + // PDO TODO: it would be nice to make unbuffered resultsets available + throw new \Exception('unbuffered queries are not yet implemented for SparkFriendFinder'); + $this->_buffered = false; + return $this; + } + + /** + * Custom escape to quote and otherwise render strings. + */ + protected function render ($value) + { + $value = $this->typeJuggle($value); + if ($value instanceof DB\Literal) { + return $value->literal(); + } + return $this->getDBH()->quote($value); + } + + /** + * Normalize values to the scheme expected by render() + */ + protected function typeJuggle ($val) + { + // promote actual PHP nulls to SQL nulls + if (is_null($val)) + $val = new DB\Null; + + if ($val instanceof DB\Literal) + return $val; + + if (is_string($val)) + return $val; + + if (is_bool($val)) { + if ($val) + $val = new DB\True; + else + $val = new DB\False; + } + + return $val; + } + + + /** + * Initial SELECT and WHERE. + * + * If a JOIN is present, it will be included. + */ + protected function _base_query () + { + $this->_default_query = 'SELECT ' . + $this->_distinctOn . ' ' . + $this->_extraselects; + + // if true, only primary table key and extra selects are selected + if ($this->_selectIdOnly) { + $this->_default_query .= $this->_table . '.' . $this->_tableKey; + } else { + $this->_default_query .= $this->_table . '.* ' . + $this->_with; + } + + $this->_default_query .= ' FROM ' . $this->_table . + $this->_join . + $this->_left_join . + ' WHERE ' . (($this->_key && $this->_id) ? $this->_key . ' = ' . $this->_id : 'true '); + + return $this->_default_query; + } + + /** + * Generates a string which selects and aliases + * each column for a given dinosaur. + */ + protected function _generate_with ($dinosaur) + { + $with = ''; + + $table = $dinosaur::getDefaultTableName(); + $columns = array_keys($dinosaur::getDefaults()); + foreach ($columns as $column) { + $with .= ", " . $table . '.' . $column . ' AS "' . $dinosaur::getBaseName() . '__' . $column . '"'; + } + + return $with; + } + +}