<?php
|
|
use \SparkLib\DB;
|
|
use \SparkLib\Fail;
|
|
|
|
/**
|
|
* This is a modified SparkFinder for use with generated relationships. It
|
|
* is automatically available through __get magic on dinosaurs with a
|
|
* corresponding has_many defined in configuration/records.php. For now it
|
|
* uses a blanket 's' on the end of the class name for plurals, like Orders,
|
|
* Customers, or OrdersStatusHistorys =[
|
|
*
|
|
* See the last section of this file for the available SQL operations.
|
|
*
|
|
* EXAMPLE 1: Find a customer's 5 most recent orders.
|
|
*
|
|
* $customer = new CustomerSaurus(id);
|
|
* $orders = $customer->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;
|
|
}
|
|
|
|
}
|