Newer
Older
var $type;
function toSql($compiler, $model=false, $alias=false) {
$A = $this->args[0];
if ($A instanceof SqlFunction)
$A = $A->toSql($compiler, $model);
else
$A = $compiler->input($A);
return sprintf('INTERVAL %s %s',
$A,
$this->func)
. ($alias ? ' AS '.$compiler->quote($alias) : '');
}
static function __callStatic($interval, $args) {
if (count($args) != 1) {
throw new InvalidArgumentException("Interval expects a single interval value");
}
return parent::__callStatic($interval, $args);
}
}
class SqlField extends SqlExpression {
var $level;
function __construct($field, $level=0) {
}
function toSql($compiler, $model=false, $alias=false) {
$L = $this->level;
while ($L--)
$compiler = $compiler->getParent();
list($field) = $compiler->getField($this->field, $model);
return $field;
}
}
class SqlCode extends SqlFunction {
function __construct($code) {
$this->code = $code;
}
function toSql($compiler, $model=false, $alias=false) {
return $this->code.($alias ? ' AS '.$alias : '');
}
}
class SqlAggregate extends SqlFunction {
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
var $func;
var $expr;
var $distinct=false;
var $constraint=false;
function __construct($func, $expr, $distinct=false, $constraint=false) {
$this->func = $func;
$this->expr = $expr;
$this->distinct = $distinct;
if ($constraint instanceof Q)
$this->constraint = $constraint;
elseif ($constraint)
$this->constraint = new Q($constraint);
}
static function __callStatic($func, $args) {
$distinct = @$args[1] ?: false;
$constraint = @$args[2] ?: false;
return new static($func, $args[0], $distinct, $constraint);
}
function toSql($compiler, $model=false, $alias=false) {
$options = array('constraint' => $this->constraint, 'model' => true);
// For DISTINCT, require a field specification — not a relationship
// specification.
$E = $this->expr;
if ($E instanceof SqlFunction) {
$field = $E->toSql($compiler, $model);
}
else {
list($field, $rmodel) = $compiler->getField($E, $model, $options);
if ($this->distinct) {
$pk = false;
$fpk = $rmodel::getMeta('pk');
foreach ($fpk as $f) {
$pk |= false !== strpos($field, $f);
}
if (!$pk) {
// Try and use the foriegn primary key
list($field) = $compiler->getField(
$this->expr . '__' . $fpk[0],
$model, $options);
}
else {
throw new OrmException(
sprintf('%s :: %s', $rmodel, $field) .
': DISTINCT aggregate expressions require specification of a single primary key field of the remote model'
);
}
}
}
return sprintf('%s(%s%s)%s', $this->func,
$this->distinct ? 'DISTINCT ' : '', $field,
$alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
}
function getFieldName() {
return strtolower(sprintf('%s__%s', $this->args[0], $this->func));
}
}
class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countable {
var $model;
var $constraints = array();
var $path_constraints = array();
var $ordering = array();
var $limit = false;
var $offset = 0;
var $related = array();
var $values = array();
var $defer = array();
var $aggregated = false;
var $extra = array();
var $distinct = array();
var $options = array();
const LOCK_EXCLUSIVE = 1;
const LOCK_SHARED = 2;
const ASC = 'ASC';
const DESC = 'DESC';
const OPT_NOSORT = 'nosort';
const OPT_NOCACHE = 'nocache';
const OPT_MYSQL_FOUND_ROWS = 'found_rows';
const ITER_MODELS = 1;
const ITER_HASH = 2;
const ITER_ROW = 3;
var $iter = self::ITER_MODELS;
var $compiler = 'MySqlCompiler';
var $query;
function __construct($model) {
$this->model = $model;
}
function filter() {
// Multiple arrays passes means OR
foreach (func_get_args() as $Q) {
$this->constraints[] = $Q instanceof Q ? $Q : new Q($Q);
return $this;
}
function exclude() {
foreach (func_get_args() as $Q) {
$this->constraints[] = $Q instanceof Q ? $Q->negate() : Q::not($Q);
/**
* Add a path constraint for the query. This is different from ::filter
* in that the constraint is added to a join clause which is normally
* built from the model meta data. The ::filter() method on the other
* hand adds the constraint to the where clause. This is generally useful
* for aggregate queries and left join queries where multiple rows might
* match a filter in the where clause and would produce incorrect results.
*
* Example:
* Find users with personal email hosted with gmail.
* >>> $Q = User::objects();
* >>> $Q->constrain(['user__emails' => new Q(['type' => 'personal']))
* >>> $Q->filter(['user__emails__address__contains' => '@gmail.com'])
*/
function constrain() {
foreach (func_get_args() as $I) {
foreach ($I as $path => $Q) {
if (!is_array($Q) && !$Q instanceof Q) {
// ->constrain(array('field__path__op' => val));
$Q = array($path => $Q);
list(, $path) = SqlCompiler::splitCriteria($path);
$path = implode('__', $path);
}
$this->path_constraints[$path][] = $Q instanceof Q ? $Q : Q::all($Q);
}
}
return $this;
}
function defer() {
foreach (func_get_args() as $f)
$this->defer[$f] = true;
return $this;
}
function order_by($order, $direction=false) {
if ($order === false)
return $this->options(array('nosort' => true));
$args = func_get_args();
if (in_array($direction, array(self::ASC, self::DESC))) {
$args = array($args[0]);
}
else
$direction = false;
$new = is_array($order) ? $order : $args;
if ($direction) {
foreach ($new as $i=>$x) {
$new[$i] = array($x, $direction);
}
}
$this->ordering = array_merge($this->ordering, $new);
function getSortFields() {
$ordering = $this->ordering;
if ($this->extra['order_by'])
$ordering = array_merge($ordering, $this->extra['order_by']);
return $ordering;
}
function lock($how=false) {
$this->lock = $how ?: self::LOCK_EXCLUSIVE;
return $this;
}
function limit($count) {
$this->limit = $count;
return $this;
}
function offset($at) {
$this->offset = $at;
return $this;
}
function isWindowed() {
return $this->limit || $this->offset || (count($this->values) + count($this->annotations) + @count($this->extra['select'])) > 1;
/**
* Fetch related fields with the query. This will result in better
* performance as related items are fetched with the root model with
* only one trip to the database.
*
* Either an array of fields can be sent as one argument, or the list of
* fields can be sent as the arguments to the function.
*
* Example:
* >>> $q = User::objects()->select_related('role');
*/
$args = func_get_args();
if (is_array($args[0]))
$args = $args[0];
$this->related = array_merge($this->related, $args);
function extra(array $extra) {
foreach ($extra as $section=>$info) {
$this->extra[$section] = array_merge($this->extra[$section] ?: array(), $info);
}
return $this;
}
function distinct() {
foreach (func_get_args() as $D)
$this->distinct[] = $D;
return $this;
}
function models() {
$this->iter = self::ITER_MODELS;
$this->values = $this->related = array();
return $this;
}
foreach (func_get_args() as $A)
$this->values[$A] = $A;
$this->iter = self::ITER_HASH;
// This disables related models
$this->related = false;
function values_flat() {
$this->values = func_get_args();
$this->iter = self::ITER_ROW;
// This disables related models
$this->related = false;
function copy() {
return clone $this;
}
return $this->getIterator()->asArray();
$list = $this->limit(1)->all();
/**
* one
*
* Finds and returns a single model instance based on the criteria in
* this QuerySet instance.
*
* Throws:
* DoesNotExist - if no such model exists with the given criteria
* ObjectNotUnique - if more than one model matches the given criteria
*
* Returns:
* (Object<Model>) a single instance of the sought model is guarenteed.
* If no such model or multiple models exist, an exception is thrown.
*/
if (count($list) == 0)
throw new DoesNotExist();
elseif (count($list) > 1)
throw new ObjectNotUnique('One object was expected; however '
.'multiple objects in the database matched the query. '
.sprintf('In fact, there are %d matching objects.', count($list))
);
// Defer to the iterator if fetching already started
if (isset($this->_iterator)) {
return $this->_iterator->count();
}
elseif (isset($this->count)) {
return $this->count;
$class = $this->compiler;
$compiler = new $class();
return $this->count = $compiler->compileCount($this);
/**
* Similar to count, except that the LIMIT and OFFSET parts are not
* considered in the counts. That is, this will return the count of rows
* if the query were not windowed with limit() and offset().
*
* For MySQL, the query will be submitted and fetched and the
* SQL_CALC_FOUND_ROWS hint will be sent in the query. Afterwards, the
* result of FOUND_ROWS() is fetched and is the result of this function.
*
* The result of this function is cached. If further changes are made
* after this is run, the changes should be made in a clone.
*/
function total() {
if (isset($this->total))
return $this->total;
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
// Optimize the query with the CALC_FOUND_ROWS if
// - the compiler supports it
// - the iterator hasn't yet been built, that is, the query for this
// statement has not yet been sent to the database
$compiler = $this->compiler;
if ($compiler::supportsOption(self::OPT_MYSQL_FOUND_ROWS)
&& !isset($this->_iterator)
) {
// This optimization requires caching
$this->options(array(
self::OPT_MYSQL_FOUND_ROWS => 1,
self::OPT_NOCACHE => null,
));
$this->exists(true);
$compiler = new $compiler();
return $this->total = $compiler->getFoundRows();
}
$query = clone $this;
$query->limit(false)->offset(false)->order_by(false);
return $this->total = $query->count();
}
function toSql($compiler, $model, $alias=false) {
// FIXME: Force root model of the compiler to $model
$exec = $this->getQuery(array('compiler' => get_class($compiler),
'parent' => $compiler, 'subquery' => true));
// Rewrite the parameter numbers so they fit the parameter numbers
// of the current parameters of the $compiler
$sql = preg_replace_callback("/:(\d+)/",
function($m) use ($compiler, $exec) {
$compiler->params[] = $exec->params[$m[1]-1];
return ':'.count($compiler->params);
}, $exec->sql);
return "({$sql})".($alias ? " AS {$alias}" : '');
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
/**
* exists
*
* Determines if there are any rows in this QuerySet. This can be
* achieved either by evaluating a SELECT COUNT(*) query or by
* attempting to fetch the first row from the recordset and return
* boolean success.
*
* Parameters:
* $fetch - (bool) TRUE if a compile and fetch should be attempted
* instead of a SELECT COUNT(*). This would be recommended if an
* accurate count is not required and the records would be fetched
* if this method returns TRUE.
*
* Returns:
* (bool) TRUE if there would be at least one record in this QuerySet
*/
function exists($fetch=false) {
if ($fetch) {
return (bool) $this[0];
}
return $this->count() > 0;
}
function annotate($annotations) {
if (!is_array($annotations))
$annotations = func_get_args();
foreach ($annotations as $name=>$A) {
if (is_int($name))
$name = $A->getFieldName();
$A->setAlias($name);
}
function aggregate($annotations) {
// Aggregate works like annotate, except that it sets up values
// fetching which will disable model creation
$this->annotate($annotations);
// Disable other fields from being fetched
$this->aggregated = true;
$this->related = false;
return $this;
}
function options($options) {
// Make an array with $options as the only key
if (!is_array($options))
$options = array($options => 1);
$this->options = array_merge($this->options, $options);
return $this;
}
function hasOption($option) {
return isset($this->options[$option]);
}
function countSelectFields() {
$count = count($this->values) + count($this->annotations);
if (isset($this->extra['select']))
foreach (@$this->extra['select'] as $S)
$count += count($S);
return $count;
}
function union(QuerySet $other, $all=true) {
// Values and values_list _must_ match for this to work
if ($this->countSelectFields() != $other->countSelectFields())
throw new OrmException('Union queries must have matching values counts');
// TODO: Clear OFFSET and LIMIT in the $other query
$this->chain[] = array($other, $all);
return $this;
}
function delete() {
$class = $this->compiler;
$compiler = new $class();
// XXX: Mark all in-memory cached objects as deleted
$ex = $compiler->compileBulkDelete($this);
$ex->execute();
return $ex->affected_rows();
}
function update(array $what) {
$class = $this->compiler;
$compiler = new $class;
$ex = $compiler->compileBulkUpdate($this, $what);
$ex->execute();
return $ex->affected_rows();
}
function __clone() {
unset($this->_iterator);
unset($this->query);
function __call($name, $args) {
if (!is_callable(array($this->getIterator(), $name)))
throw new OrmException('Call to undefined method QuerySet::'.$name);
return $args
? call_user_func_array(array($this->getIterator(), $name), $args)
: call_user_func(array($this->getIterator(), $name));
}
function getIterator($iterator=false) {
if (!isset($this->_iterator)) {
$class = $iterator ?: $this->getIteratorClass();
$it = new $class($this);
if (!$this->hasOption(self::OPT_NOCACHE)) {
if ($this->iter == self::ITER_MODELS)
// Add findFirst() and such
$it = new ModelResultSet($it);
else
$it = new CachedResultSet($it);
}
else {
$it = $it->getIterator();
}
$this->_iterator = $it;
}
function getIteratorClass() {
switch ($this->iter) {
case self::ITER_MODELS:
return 'ModelInstanceManager';
case self::ITER_HASH:
return 'HashArrayIterator';
case self::ITER_ROW:
return 'FlatArrayIterator';
}
}
// ArrayAccess interface
function offsetExists($offset) {
return $this->getIterator()->offsetExists($offset);
}
function offsetGet($offset) {
return $this->getIterator()->offsetGet($offset);
}
function offsetUnset($a) {
throw new Exception(__('QuerySet is read-only'));
throw new Exception(__('QuerySet is read-only'));
return (string) $this->getQuery();
function getQuery($options=array()) {
if (isset($this->query))
return $this->query;
// Load defaults from model
$model = $this->model;
$meta = $model::getMeta();
$options += $this->options;
if ($options['nosort'])
$query->ordering = array();
elseif (!$query->ordering && $meta['ordering'])
$query->ordering = $meta['ordering'];
if (false !== $query->related && !$query->related && !$query->values && $meta['select_related'])
$query->related = $meta['select_related'];
if (!$query->defer && $meta['defer'])
$query->defer = $meta['defer'];
$class = $options['compiler'] ?: $this->compiler;
$compiler = new $class($options);
$this->query = $compiler->compileSelect($query);
/**
* Fetch a model class which can be used to render the QuerySet as a
* subquery to be used as a JOIN.
*/
function asView() {
$unique = spl_object_hash($this);
$classname = "QueryView{$unique}";
if (class_exists($classname))
return $classname;
$class = <<<EOF
class {$classname} extends VerySimpleModel {
static \$meta = array(
'view' => true,
);
static \$queryset;
static function getQuery(\$compiler) {
return ' ('.static::\$queryset->getQuery().') ';
}
static function getSqlAddParams(\$compiler) {
return static::\$queryset->toSql(\$compiler, self::\$queryset->model);
}
EOF;
eval($class); // Ugh
$classname::$queryset = $this;
return $classname;
}
function serialize() {
$info = get_object_vars($this);
unset($info['query']);
unset($info['limit']);
unset($info['offset']);
unset($info['_iterator']);
return serialize($info);
}
function unserialize($data) {
$data = unserialize($data);
foreach ($data as $name => $val) {
$this->{$name} = $val;
}
}
class DoesNotExist extends Exception {}
class ObjectNotUnique extends Exception {}
class CachedResultSet
extends BaseList
implements ArrayAccess {
protected $inner;
protected $eoi = false;
function __construct(IteratorAggregate $iterator) {
$this->inner = $iterator->getIterator();
function fillTo($level) {
while (!$this->eoi && count($this->storage) < $level) {
if (!$this->inner->valid()) {
$this->eoi = true;
break;
}
$this->storage[] = $this->inner->current();
$this->inner->next();
}
function asArray() {
$this->fillTo(PHP_INT_MAX);
function getCache() {
return $this->storage;
}
function reset() {
$this->eoi = false;
$this->storage = array();
// XXX: Should the inner be recreated to refetch?
$this->inner->rewind();
function getIterator() {
$this->asArray();
return new ArrayIterator($this->storage);
}
function offsetExists($offset) {
$this->fillTo($offset+1);
return count($this->storage) > $offset;
$this->fillTo($offset+1);
return $this->storage[$offset];
throw new Exception(__('QuerySet is read-only'));
throw new Exception(__('QuerySet is read-only'));
return count($this->storage);
/**
* Sort the instrumented list in place. This would be useful to change the
* sorting order of the items in the list without fetching the list from
* the database again.
*
* Parameters:
* $key - (callable|int) A callable function to produce the sort keys
* or one of the SORT_ constants used by the array_multisort
* function
* $reverse - (bool) true if the list should be sorted descending
*
* Returns:
* This instrumented list for chaining and inlining.
*/
function sort($key=false, $reverse=false) {
// Fetch all records into the cache
$this->asArray();
parent::sort($key, $reverse);
return $this;
}
/**
* Reverse the list item in place. Returns this object for chaining
*/
function reverse() {
$this->asArray();
return parent::reverse();
class ModelResultSet
extends CachedResultSet {
/**
* Find the first item in the current set which matches the given criteria.
* This would be used in favor of ::filter() which might trigger another
* database query. The criteria is intended to be quite simple and should
* not traverse relationships which have not already been fetched.
* Otherwise, the ::filter() or ::window() methods would provide better
* performance.
*
* Example:
* >>> $a = new User();
* >>> $a->roles->add(Role::lookup(['name' => 'administator']));
* >>> $a->roles->findFirst(['roles__name__startswith' => 'admin']);
* <Role: administrator>
*/
function findFirst($criteria) {
$records = $this->findAll($criteria, 1);
return count($records) > 0 ? $records[0] : null;
}
/**
* Find all the items in the current set which match the given criteria.
* This would be used in favor of ::filter() which might trigger another
* database query. The criteria is intended to be quite simple and should
* not traverse relationships which have not already been fetched.
* Otherwise, the ::filter() or ::window() methods would provide better
* performance, as they can provide results with one more trip to the
* database.
*/
function findAll($criteria, $limit=false) {
$records = new ListObject();
foreach ($this as $record) {
$matches = true;
foreach ($criteria as $field=>$check) {
if (!SqlCompiler::evaluate($record, $check, $field)) {
$matches = false;
break;
}
}
if ($matches)
$records[] = $record;
if ($limit && count($records) == $limit)
break;
}
return $records;
}
}
class ModelInstanceManager
implements IteratorAggregate {
var $queryset;
var $model;
var $map;
static $objectCache = array();
function __construct(QuerySet $queryset) {
$this->queryset = $queryset;
$this->model = $queryset->model;
}
function cache($model) {
$key = sprintf('%s.%s',
$model::$meta->model, implode('.', $model->get('pk')));
self::$objectCache[$key] = $model;
}
/**
* uncache
*
* Drop the cached reference to the model. If the model is deleted
* database-side. Lookups for the same model should not be short
* circuited to retrieve the cached reference.
*/
static function uncache($model) {
$key = sprintf('%s.%s',
$model::$meta->model, implode('.', $model->pk));
unset(self::$objectCache[$key]);
}
static function flushCache() {
self::$objectCache = array();
}
static function checkCache($modelClass, $fields) {
$key = $modelClass::$meta->model;
foreach ($modelClass::getMeta('pk') as $f)
$key .= '.'.$fields[$f];
return @self::$objectCache[$key];
}
/**
* getOrBuild
*
* Builds a new model from the received fields or returns the model
* already stashed in the model cache. Caching helps to ensure that
* multiple lookups for the same model identified by primary key will
* fetch the exact same model. Therefore, changes made to the model
* anywhere in the project will be reflected everywhere.
*
* For annotated models (models build from querysets with annotations),
* the built or cached model is wrapped in an AnnotatedModel instance.
* The annotated fields are in the AnnotatedModel instance and the
* database-backed fields are managed by the Model instance.
*/
function getOrBuild($modelClass, $fields, $cache=true) {
// Check for NULL primary key, used with related model fetching. If
// the PK is NULL, then consider the object to also be NULL
foreach ($modelClass::getMeta('pk') as $pkf) {
if (!isset($fields[$pkf])) {
return null;
}
}
$annotations = $this->queryset->annotations;
$extras = array();
// For annotations, drop them from the $fields list and add them to
// an $extras list. The fields passed to the root model should only
// be the root model's fields. The annotated fields will be wrapped
// using an AnnotatedModel instance.
if ($annotations && $modelClass == $this->model) {
if (array_key_exists($name, $fields)) {
$extras[$name] = $fields[$name];
unset($fields[$name]);
}
}
}
// Check the cache for the model instance first
if (!($m = self::checkCache($modelClass, $fields))) {
// Construct and cache the object
$m = $modelClass::__hydrate($fields);
// XXX: defer may refer to fields not in this model
$m->__deferred__ = $this->queryset->defer;
$m->__onload();
if ($cache)
$this->cache($m);
}
// Wrap annotations in an AnnotatedModel
if ($extras) {
$m = AnnotatedModel::wrap($m, $extras, $modelClass);
// TODO: If the model has deferred fields which are in $fields,
// those can be resolved here
return $m;
}
/**
* buildModel
*
* This method builds the model including related models from the record
* received. For related recordsets, a $map should be setup inside this
* object prior to using this method. The $map is assumed to have this
* configuration:
*
* array(array(<fieldNames>, <modelClass>, <relativePath>))
*
* Where $modelClass is the name of the foreign (with respect to the
* root model ($this->model), $fieldNames is the number and names of
* fields in the row for this model, $relativePath is the path that
* describes the relationship between the root model and this model,
* 'user__account' for instance.
function buildModel($row, $cache=true) {
if ($this->map) {
if ($this->model != $this->map[0][1])
throw new OrmException('Internal select_related error');
$offset = 0;
foreach ($this->map as $info) {
@list($fields, $model_class, $path) = $info;
$values = array_slice($row, $offset, count($fields));
$record = array_combine($fields, $values);
if (!$path) {
// Build the root model
$model = $this->getOrBuild($this->model, $record, $cache);
$i = 0;
// Traverse the declared path and link the related model
$tail = array_pop($path);
$m = $model;
foreach ($path as $field) {
if (!($m = $m->get($field)))
break;
if ($m) {
// Only apply cache setting to the root model.
// Reference models should use caching
$m->set($tail, $this->getOrBuild($model_class, $record, $cache));
}
$model = $this->getOrBuild($this->model, $row, $cache);
function getIterator() {
$this->resource = $this->queryset->getQuery();
$this->map = $this->resource->getMap();
$cache = !$this->queryset->hasOption(QuerySet::OPT_NOCACHE);
$this->resource->setBuffered($cache);
$func = ($this->map) ? 'getRow' : 'getArray';
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
$func = array($this->resource, $func);
return new CallbackSimpleIterator(function() use ($func, $cache) {
global $StopIteration;
if ($row = $func())
return $this->buildModel($row, $cache);
$this->resource->close();
throw $StopIteration;
});
}
}
class CallbackSimpleIterator
implements Iterator {
var $current;
var $eoi;
var $callback;
var $key = -1;
function __construct($callback) {
assert(is_callable($callback));
$this->callback = $callback;
function rewind() {
$this->eoi = false;
$this->next();
}
function key() {
return $this->key;
}
function valid() {
if (!isset($this->eoi))
$this->rewind();
return !$this->eoi;
}