Newer
Older
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->iterator = 'ModelInstanceManager';
$this->values = $this->related = array();
return $this;
}
foreach (func_get_args() as $A)
$this->values[$A] = $A;
// This disables related models
$this->related = false;
function values_flat() {
$this->values = func_get_args();
$this->iterator = 'FlatArrayIterator';
// This disables related models
$this->related = false;
function all() {
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.
*/
function one() {
$list = $this->all();
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);
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}" : '');
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
/**
* 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 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);
}
// IteratorAggregate interface
function getIterator() {
$class = $this->iterator;
$this->_iterator = new $class($this);
return $this->_iterator;
}
// 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;
if (!$options['nosort'] && !$query->ordering && $model::getMeta('ordering'))
$query->ordering = $model::getMeta('ordering');
if (false !== $query->related && !$query->values && $model::getMeta('select_related'))
$query->related = $model::getMeta('select_related');
if (!$query->defer && $model::getMeta('defer'))
$query->defer = $model::getMeta('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}";
$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 {}
abstract class ResultSet implements Iterator, ArrayAccess, Countable {
var $resource;
var $position = 0;
var $queryset;
function __construct($queryset=false) {
$this->queryset = $queryset;
if ($queryset) {
$this->model = $queryset->model;
function prime() {
if (!isset($this->resource) && $this->queryset)
$this->resource = $this->queryset->getQuery();
}
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
abstract function fillTo($index);
function asArray() {
$this->fillTo(PHP_INT_MAX);
return $this->cache;
}
// Iterator interface
function rewind() {
$this->position = 0;
}
function current() {
$this->fillTo($this->position);
return $this->cache[$this->position];
}
function key() {
return $this->position;
}
function next() {
$this->position++;
}
function valid() {
$this->fillTo($this->position);
return count($this->cache) > $this->position;
}
// ArrayAccess interface
function offsetExists($offset) {
$this->fillTo($offset);
return $this->position >= $offset;
}
function offsetGet($offset) {
$this->fillTo($offset);
return $this->cache[$offset];
}
function offsetUnset($a) {
throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
}
function offsetSet($a, $b) {
throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
}
// Countable interface
function count() {
return count($this->asArray());
}
}
class ModelInstanceManager extends ResultSet {
var $model;
var $map;
static $objectCache = array();
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 = new $modelClass($fields);
// XXX: defer may refer to fields not in this model
$m->__deferred__ = $this->queryset->defer;
$m->__onload();
if ($cache)
$this->cache($m);
}
elseif (get_class($m) != $modelClass) {
// Change the class of the object to be returned to match what
// was expected
// TODO: Emit a warning?
$m = new $modelClass($m->ht);
}
// Wrap annotations in an AnnotatedModel
if ($extras) {
$m = new AnnotatedModel($m, $extras);
// 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) {
// TODO: Traverse to foreign keys
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);
$i = 0;
// Traverse the declared path and link the related model
$tail = array_pop($path);
$m = $model;
foreach ($path as $field) {
$m = $m->get($field);
}
$m->set($tail, $this->getOrBuild($model_class, $record));
$model = $this->getOrBuild($this->model, $row);
$func = ($this->map) ? 'getRow' : 'getArray';
while ($this->resource && $index >= count($this->cache)) {
if ($row = $this->resource->{$func}()) {
$this->cache[] = $this->buildModel($row);
} else {
$this->resource->close();
$this->resource = false;
function prime() {
parent::prime();
if ($this->resource) {
$this->map = $this->resource->getMap();
}
}
class FlatArrayIterator extends ResultSet {
while ($this->resource && $index >= count($this->cache)) {
if ($row = $this->resource->getRow()) {
} else {
$this->resource->close();
$this->resource = false;
break;
}
}
}
}
class HashArrayIterator extends ResultSet {
function fillTo($index) {
while ($this->resource && $index >= count($this->cache)) {
if ($row = $this->resource->getArray()) {
$this->cache[] = $row;
} else {
$this->resource->close();
$this->resource = false;
break;
}
}
}
}
class InstrumentedList extends ModelInstanceManager {
var $key;
function __construct($fkey, $queryset=false) {
list($model, $this->key) = $fkey;
$queryset = $model::objects()->filter($this->key);
if ($related = $model::getMeta('select_related'))
$queryset->select_related($related);
}
parent::__construct($queryset);
function add($object, $at=false) {
if (!$object || !$object instanceof $this->model)
throw new Exception(sprintf(
'Attempting to add invalid object to list. Expected <%s>, but got <%s>',
$this->model,
get_class($object)
));
foreach ($this->key as $field=>$value)
$object->set($field, $value);
if (!$object->__new__)
$object->save();
if ($at !== false)
$this->cache[$at] = $object;
else
$this->cache[] = $object;
function remove($object, $delete=true) {
if ($delete)
$object->delete();
else
foreach ($this->key as $field=>$value)
$object->set($field, null);
function reset() {
$this->cache = array();
/**
* Reduce the list to a subset using a simply key/value constraint. New
* items added to the subset will have the constraint automatically
* added to all new items.
*/
function window($constraint) {
$model = $this->model;
$fields = $model::getMeta('fields');
$key = $this->key;
foreach ($constraint as $field=>$value) {
if (!is_string($field) || false === in_array($field, $fields))
throw new OrmException('InstrumentedList windowing must be performed on local fields only');
$key[$field] = $value;
}
return new static(array($this->model, $key), $this->filter($constraint));
}
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
/**
* 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(array $criteria) {
foreach ($this as $record) {
$matches = true;
foreach ($criteria as $field=>$check) {
if (!SqlCompiler::evaluate($record, $field, $check)) {
$matches = false;
break;
}
}
if ($matches)
return $record;
}
}
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
/**
* 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();
if (is_callable($key)) {
array_multisort(
array_map($key, $this->cache),
$reverse ? SORT_DESC : SORT_ASC,
$this->cache);
}
elseif ($key) {
array_multisort($this->cache,
$reverse ? SORT_DESC : SORT_ASC, $key);
}
elseif ($reverse) {
rsort($this->cache);
}
else
sort($this->cache);
return $this;
}
/**
* Reverse the list item in place. Returns this object for chaining
*/
function reverse() {
$this->asArray();
array_reverse($this->cache);
return $this;
}
// Save all changes made to any list items
function saveAll() {
foreach ($this as $I)
if (!$I->save())
return false;
return true;
}
// QuerySet delegates
function count() {
return $this->objects()->count();
}
function exists() {
return $this->queryset->exists();
}
function expunge() {
if ($this->queryset->delete())
$this->reset();
function update(array $what) {
return $this->queryset->update($what);
}
// Fetch a new QuerySet
function objects() {
return clone $this->queryset;
function offsetUnset($a) {
$this->fillTo($a);
$this->cache[$a]->delete();
}
function offsetSet($a, $b) {
$this->fillTo($a);
$this->cache[$a]->delete();
$this->add($b, $a);
function __call($what, $how) {
return call_user_func_array(array($this->objects(), $what), $how);
var $joins = array();
var $aliases = array();
var $alias_num = 1;
function __construct($options=false) {
if ($options)
$this->options = array_merge($this->options, $options);
if ($options['subquery'])
$this->alias_num += 150;
}
function getParent() {
return $this->options['parent'];
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
/**
* Split a criteria item into the identifying pieces: path, field, and
* operator.
*/
static function splitCriteria($criteria) {
static $operators = array(
'exact' => 1, 'isnull' => 1,
'gt' => 1, 'lt' => 1, 'gte' => 1, 'lte' => 1,
'contains' => 1, 'like' => 1, 'startswith' => 1, 'endswith' => 1,
'in' => 1, 'intersect' => 1,
'hasbit' => 1,
);
$path = explode('__', $criteria);
if (!isset($options['table'])) {
$field = array_pop($path);
if (isset($operators[$field])) {
$operator = $field;
$field = array_pop($path);
}
}
return array($field, $path, $operator ?: 'exact');
}
/**
* Check if the values match given the operator.
*
* Throws:
* OrmException - if $operator is not supported
*/
static function evaluate($record, $field, $check) {
static $ops; if (!isset($ops)) { $ops = array(
'exact' => function($a, $b) { return is_string($a) ? strcasecmp($a, $b) == 0 : $a == $b; },
'isnull' => function($a, $b) { return is_null($a) == $b; },
'gt' => function($a, $b) { return $a > $b; },
'gte' => function($a, $b) { return $a >= $b; },
'lt' => function($a, $b) { return $a < $b; },
'lte' => function($a, $b) { return $a <= $b; },
'contains' => function($a, $b) { return stripos($a, $b) !== false; },
'startswith' => function($a, $b) { return stripos($a, $b) === 0; },
'hasbit' => function($a, $b) { return $a & $b == $b; },
); }
list($field, $path, $operator) = self::splitCriteria($field);
if (!isset($ops[$operator]))
throw new OrmException($operator.': Unsupported operator');
if ($path)
$record = $record->getByPath($path);
return $ops[$operator]($record->get($field), $check);
}
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
/**
* Handles breaking down a field or model search descriptor into the
* model search path, field, and operator parts. When used in a queryset
* filter, an expression such as
*
* user__email__hostname__contains => 'foobar'
*
* would be broken down to search from the root model (passed in,
* perhaps a ticket) to the user and email models by inspecting the
* model metadata 'joins' property. The 'constraint' value found there
* will be used to build the JOIN sql clauses.
*
* The 'hostname' will be the field on 'email' model that should be
* compared in the WHERE clause. The comparison should be made using a
* 'contains' function, which in MySQL, might be implemented using
* something like "<lhs> LIKE '%foobar%'"
*
* This function will rely heavily on the pushJoin() function which will
* handle keeping track of joins made previously in the query and
* therefore prevent multiple joins to the same table for the same
* reason. (Self joins are still supported).
*
* Comparison functions supported by this function are defined for each
* respective SqlCompiler subclass; however at least these functions
* should be defined:
*
* function a__function => b
* ----------+------------------------------------------------
* exact | a is exactly equal to b
* gt | a is greater than b
* lte | b is greater than a
* lt | a is less than b
* gte | b is less than a
* ----------+------------------------------------------------
* contains | (string) b is contained within a
* statswith | (string) first len(b) chars of a are exactly b
* endswith | (string) last len(b) chars of a are exactly b
* like | (string) a matches pattern b
* ----------+------------------------------------------------
* in | a is in the list or the nested queryset b
* ----------+------------------------------------------------
* isnull | a is null (if b) else a is not null
*
* If no comparison function is declared in the field descriptor,
* 'exact' is assumed.
*
* Parameters:
* $field - (string) name of the field to join
* $model - (VerySimpleModel) root model for references in the $field
* parameter
* $options - (array) extra options for the compiler
* 'table' => return the table alias rather than the field-name
* 'model' => return the target model class rather than the operator
* 'constraint' => extra constraint for join clause
*
* Returns:
* (mixed) Usually array<field-name, operator> where field-name is the
* name of the field in the destination model, and operator is the
* requestion comparison method.
*/
function getField($field, $model, $options=array()) {
// Break apart the field descriptor by __ (double-underbars). The
// first part is assumed to be the root field in the given model.
// The parts after each of the __ pieces are links to other tables.
// The last item (after the last __) is allowed to be an operator
// specifiction.
list($field, $parts, $op) = static::splitCriteria($field);
$operator = static::$operators[$op];
$rootModel = $model;
// Call pushJoin for each segment in the join path. A new JOIN
// fragment will need to be emitted and/or cached
$joins = array();
$push = function($p, $model) use (&$joins, &$path) {
$J = $model::getMeta('joins');
if (!($info = $J[$p])) {
throw new OrmException(sprintf(
'Model `%s` does not have a relation called `%s`',
$model, $p));
}
$crumb = $path;
$path = ($path) ? "{$path}__{$p}" : $p;
$joins[] = array($crumb, $path, $model, $info);
// Roll to foreign model
return $info['fkey'];
foreach ($parts as $p) {
list($model) = $push($p, $model);
}
// If comparing a relationship, join the foreign table
// This is a comparison with a relationship — use the foreign key
$J = $model::getMeta('joins');
if (isset($J[$field])) {
list($model, $field) = $push($field, $model);
}
// Apply the joins list to $this->pushJoin
$last = count($joins) - 1;
$constraint = false;
foreach ($joins as $i=>$A) {
// Add the conststraint as the last arg to the last join
if ($i == $last)
$constraint = $options['constraint'];
$alias = $this->pushJoin($A[0], $A[1], $A[2], $A[3], $constraint);
}
if (!isset($alias)) {
// Determine the alias for the root model table
$alias = (isset($this->joins['']))
? $this->joins['']['alias']
: $this->quote($rootModel::getMeta('table'));
if (isset($options['table']) && $options['table'])
elseif (isset($this->annotations[$field]))
$field = $this->annotations[$field];
elseif ($alias)
$field = $alias.'.'.$this->quote($field);
if (isset($options['model']) && $options['model'])
$operator = $model;
/**
* Uses the compiler-specific `compileJoin` function to compile the join
* statement fragment, and caches the result in the local $joins list. A
* new alias is acquired using the `nextAlias` function which will be
* associated with the join. If the same path is requested again, the
* algorithm is short-circuited and the originally-assigned table alias
* is returned immediately.
*/
function pushJoin($tip, $path, $model, $info, $constraint=false) {
// TODO: Build the join statement fragment and return the table
// alias. The table alias will be useful where the join is used in
// the WHERE and ORDER BY clauses
// If the join already exists for the statement-being-compiled, just
// return the alias being used.
if (!$constraint && isset($this->joins[$path]))
return $this->joins[$path]['alias'];
// TODO: Support only using aliases if necessary. Use actual table
// names for everything except oddities like self-joins
$alias = $this->nextAlias();
// Keep an association between the table alias and the model. This
// will make model construction much easier when we have the data
// and the table alias from the database.
$this->aliases[$alias] = $model;
// TODO: Stash joins and join constraints into local ->joins array.
// This will be useful metadata in the executor to construct the
// final models for fetching
// TODO: Always use a table alias. This will further help with
// coordination between the data returned from the database (where
// table alias is available) and the corresponding data.
$T = array('alias' => $alias);
$this->joins[$path] = $T;
$this->joins[$path]['sql'] = $this->compileJoin($tip, $model, $alias, $info, $constraint);
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
/**
* compileQ
*
* Build a constraint represented in an arbitrarily nested Q instance.
* The placement of the compiled constraint is also considered and
* represented in the resulting CompiledExpression instance.
*
* Parameters:
* $Q - (Q) constraint represented in a Q instance
* $model - (VerySimpleModel) root model for all the field references in
* the Q instance
* $slot - (int) slot for inputs to be placed. Useful to differenciate
* inputs placed in the joins and where clauses for SQL engines
* which do not support named parameters.
*
* Returns:
* (CompiledExpression) object containing the compiled expression (with
* AND, OR, and NOT operators added). Furthermore, the $type attribute
* of the CompiledExpression will allow the compiler to place the
* constraint properly in the WHERE or HAVING clause appropriately.
*/
function compileQ(Q $Q, $model, $slot=false) {
$type = CompiledExpression::TYPE_WHERE;
foreach ($Q->constraints as $field=>$value) {
// Handle nested constraints
if ($value instanceof Q) {
$filter[] = $T = $this->compileQ($value, $model, $slot);
// Bubble up HAVING constraints
if ($T instanceof CompiledExpression
&& $T->type == CompiledExpression::TYPE_HAVING)
$type = $T->type;
// Handle relationship comparisons with model objects
elseif ($value instanceof VerySimpleModel) {
$criteria = array();
foreach ($value->pk as $f=>$v) {
$f = $field . '__' . $f;
$criteria[$f] = $v;
}
$filter[] = $this->compileQ(new Q($criteria), $model, $slot);
}
// Handle simple field = <value> constraints
list($field, $op) = $this->getField($field, $model);
if ($field instanceof SqlAggregate) {
// This constraint has to go in the HAVING clause