Newer
Older
if ($queryset) {
$this->model = $queryset->model;
$this->resource = $queryset->getQuery();
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
}
}
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 __construct($queryset=false) {
parent::__construct($queryset);
if ($queryset) {
$this->map = $this->resource->getMap();
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
function cache($model) {
$model::_inspect();
$key = sprintf('%s.%s',
$model::$meta->model, implode('.', $model->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 checkCache($modelClass, $fields) {
foreach ($modelClass::$meta['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::$meta['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 (isset($fields[$name])) {
$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);
}
// 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 = null;
break;
}
}
}
}
class FlatArrayIterator extends ResultSet {
function __construct($queryset) {
$this->resource = $queryset->getQuery();
}
function fillTo($index) {
while ($this->resource && $index >= count($this->cache)) {
if ($row = $this->resource->getRow()) {
} else {
$this->resource->close();
$this->resource = null;
break;
}
}
}
}
class HashArrayIterator extends ResultSet {
function __construct($queryset) {
$this->resource = $queryset->getQuery();
}
function fillTo($index) {
while ($this->resource && $index >= count($this->cache)) {
if ($row = $this->resource->getArray()) {
$this->cache[] = $row;
} else {
$this->resource->close();
$this->resource = null;
break;
}
}
}
}
class InstrumentedList extends ModelInstanceManager {
function __construct($fkey, $queryset=false) {
list($model, $this->key, $this->id) = $fkey;
if (!$queryset)
$queryset = $model::objects()->filter(array($this->key=>$this->id));
parent::__construct($queryset);
if (!$this->id)
$this->resource = null;
}
function add($object, $at=false) {
if (!$object || !$object instanceof $this->model)
throw new Exception(__('Attempting to add invalid object to list'));
$object->set($this->key, $this->id);
if ($at !== false)
$this->cache[$at] = $object;
else
$this->cache[] = $object;
}
function remove($object) {
$object->delete();
}
function reset() {
$this->cache = array();
}
// QuerySet delegates
function count() {
return $this->queryset->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);
}
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
/**
* 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.
$parts = explode('__', $field);
$operator = static::$operators['exact'];
if (!isset($options['table'])) {
if (array_key_exists($field, static::$operators)) {
$operator = static::$operators[$field];
$field = array_pop($parts);
}
// Determine the alias for the root model table
$alias = (isset($this->joins['']))
? $this->joins['']['alias']
: $this->quote($model::$meta['table']);
// Call pushJoin for each segment in the join path. A new JOIN
// fragment will need to be emitted and/or cached
$self = $this;
$push = function($p, $path, $extra=false) use (&$model, $self) {
$model::_inspect();
if (!($info = $model::$meta['joins'][$p])) {
throw new OrmException(sprintf(
'Model `%s` does not have a relation called `%s`',
$model, $p));
}
$crumb = implode('__', $path);
$path[] = $p;
$tip = implode('__', $path);
$alias = $self->pushJoin($crumb, $tip, $model, $info, $extra);
// Roll to foreign model
foreach ($info['constraint'] as $local => $foreign) {
list($model, $f) = explode('.', $foreign);
if (class_exists($model))
break;
return array($alias, $f);
};
foreach ($parts as $i=>$p) {
list($alias) = $push($p, $path, @$options['constraint']);
$path[] = $p;
}
// If comparing a relationship, join the foreign table
// This is a comparison with a relationship — use the foreign key
if (isset($model::$meta['joins'][$field])) {
list($alias, $field) = $push($field, $path, @$options['constraint']);
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;
$T['sql'] = $this->compileJoin($tip, $model, $alias, $info, $constraint);
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
/**
* 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
$field = $field->toSql($this, $model);
$type = CompiledExpression::TYPE_HAVING;
}
if ($value === null)
$filter[] = sprintf('%s IS NULL', $field);
// Allow operators to be callable rather than sprintf
// strings
$filter[] = call_user_func($op, $field, $value, $model);
$filter[] = sprintf($op, $field, $this->input($value, $slot));
$glue = $Q->isOred() ? ' OR ' : ' AND ';
$clause = implode($glue, $filter);
if (count($filter) > 1)
$clause = '(' . $clause . ')';
if ($Q->isNegated())
$clause = 'NOT '.$clause;
return new CompiledExpression($clause, $type);
}
function compileConstraints($where, $model) {
$constraints = array();
foreach ($where as $Q) {
$constraints[] = $this->compileQ($Q, $model);
}
}
function getParams() {
return $this->params;
}
function getJoins($queryset) {
$sql = '';
foreach ($this->joins as $j)
$sql .= $j['sql'];
// Add extra items from QuerySet
if (isset($queryset->extra['tables'])) {
foreach ($queryset->extra['tables'] as $S) {
$join = ' JOIN ';
// Left joins require an ON () clause
if ($lastparen = strrpos($S, '(')) {
if (preg_match('/\bon\b/i', substr($S, $lastparen - 4, 4)))
$join = ' LEFT' . $join;
}
$sql .= $join.$S;
}
}
return $sql;
}
function nextAlias() {
// Use alias A1-A9,B1-B9,...
$alias = chr(65 + (int)($this->alias_num / 9)) . $this->alias_num % 9;
$this->alias_num++;
return $alias;
}
}
class CompiledExpression /* extends SplString */ {
const TYPE_WHERE = 0x0001;
const TYPE_HAVING = 0x0002;
var $text = '';
function __construct($clause, $type=self::TYPE_WHERE) {
$this->text = $clause;
$this->type = $type;
}
function __toString() {
return $this->text;
}
}
static $compiler = 'MySqlCompiler';
function __construct($info) {
}
function connect() {
}
// Gets a compiler compatible with this database engine that can compile
// and execute a queryset or DML request.
static function getCompiler() {
$class = static::$compiler;
return new $class();
static function delete(VerySimpleModel $model) {
return static::getCompiler()->compileDelete($model);
}
static function save(VerySimpleModel $model) {
$compiler = static::getCompiler();
if ($model->__new__)
return $compiler->compileInsert($model);
else
return $compiler->compileUpdate($model);
}
}
class MySqlCompiler extends SqlCompiler {
// Consts for ::input()
const SLOT_JOINS = 1;
const SLOT_WHERE = 2;
protected $input_join_count = 0;
static $operators = array(
'exact' => '%1$s = %2$s',
'contains' => array('self', '__contains'),
'startswith' => array('self', '__startswith'),
'endswith' => array('self', '__endswith'),
'gt' => '%1$s > %2$s',
'lt' => '%1$s < %2$s',
'gte' => '%1$s >= %2$s',
'lte' => '%1$s <= %2$s',
'isnull' => array('self', '__isnull'),
'intersect' => array('self', '__find_in_set'),
// Thanks, http://stackoverflow.com/a/3683868
function like_escape($what, $e='\\') {
return str_replace(array($e, '%', '_'), array($e.$e, $e.'%', $e.'_'), $what);
}
function __contains($a, $b) {
# {%a} like %{$b}%
# Escape $b
$b = $this->like_escape($b);
return sprintf('%s LIKE %s', $a, $this->input("%$b%"));
}
function __startswith($a, $b) {
$b = $this->like_escape($b);
return sprintf('%s LIKE %s', $a, $this->input("$b%"));
}
function __endswith($a, $b) {
$b = $this->like_escape($b);
return sprintf('%s LIKE %s', $a, $this->input("%$b"));
}
function __in($a, $b) {
if (is_array($b)) {
$vals = array_map(array($this, 'input'), $b);
$b = implode(', ', $vals);
}
// MySQL doesn't support LIMIT or OFFSET in subqueries. Instead, add
// add the constraint to the join
elseif ($b instanceof QuerySet && $b->isWindowed()) {
$f1 = $b->values[0];
$view = $b->asView();
$alias = $this->pushJoin($view, $a, $view, array('constraint'=>array()));
return sprintf('%s = %s.%s', $a, $alias, $this->quote($f1));
}
else {
$b = $this->input($b);
}
function __isnull($a, $b) {
return $b
? sprintf('%s IS NULL', $a)
: sprintf('%s IS NOT NULL', $a);
}
function __find_in_set($a, $b) {
if (is_array($b)) {
$sql = array();
foreach (array_map(array($this, 'input'), $b) as $b) {
$sql[] = sprintf('FIND_IN_SET(%s, %s)', $b, $a);
}
$parens = count($sql) > 1;
$sql = implode(' OR ', $sql);
return $parens ? ('('.$sql.')') : $sql;
}
return sprintf('FIND_IN_SET(%s, %s)', $b, $a);
}
function compileJoin($tip, $model, $alias, $info, $extra=false) {
$constraints = array();
$join = ' JOIN ';
if (isset($info['null']) && $info['null'])
$join = ' LEFT'.$join;
if (isset($this->joins[$tip]))
$table = $this->joins[$tip]['alias'];
else
$table = $this->quote($model::$meta['table']);
foreach ($info['constraint'] as $local => $foreign) {
list($rmodel, $right) = explode('.', $foreign);
// Support a constant constraint with
// "'constant'" => "Model.field_name"
if ($local[0] == "'") {
$constraints[] = sprintf("%s.%s = %s",
$alias, $this->quote($right),
$this->input(trim($local, '\'"'), self::SLOT_JOINS)
// Support local constraint
// field_name => "'constant'"
elseif ($foreign[0] == "'" && !$right) {
$constraints[] = sprintf("%s.%s = %s",
$table, $this->quote($local),
$this->input(trim($foreign, '\'"'), self::SLOT_JOINS)
);
}
else {
$constraints[] = sprintf("%s.%s = %s.%s",
$table, $this->quote($local), $alias,
$this->quote($right)
);
}
// Support extra join constraints
if ($extra instanceof Q) {
$constraints[] = $this->compileQ($extra, $model, self::SLOT_JOINS);
}
if (!isset($rmodel))
$rmodel = $model;
// Support inline views
$table = ($rmodel::$meta['view'])
? $rmodel::getQuery($this)
: $this->quote($rmodel::$meta['table']);
$base = $join.$table.$alias;
if ($constraints)
$base .= ' ON ('.implode(' AND ', $constraints).')';
return $base;
/**
* input
*
* Generate a parameterized input for a database query. Input value is
* received by reference to avoid copying.
*
* Parameters:
* $what - (mixed) value to be sent to the database. No escaping is
* necessary. Pass a raw value here.
* $slot - (int) clause location of the input in compiled SQL statement.
* Currently, SLOT_JOINS and SLOT_WHERE is supported. SLOT_JOINS
* inputs are inserted ahead of the SLOT_WHERE inputs as the joins
* come logically before the where claused in the finalized
* statement.
*
* Returns:
* (string) token to be placed into the compiled SQL statement. For
* MySQL, this is always the string '?'.
*/
function input($what, $slot=false) {
if ($what instanceof QuerySet) {
$q = $what->getQuery(array('nosort'=>true));
$this->params = array_merge($this->params, $q->params);
return $q->sql;
elseif ($what instanceof SqlFunction) {
return $what->toSql($this);
elseif (!isset($what)) {
return 'NULL';
}
switch ($slot) {
case self::SLOT_JOINS:
// This should be inserted before the WHERE inputs
array_splice($this->params, $this->input_join_count++, 0,
array($what));
break;
default:
$this->params[] = $what;
}
}
function quote($what) {
return "`$what`";
}
/**
* getWhereClause
*
* This builds the WHERE ... part of a DML statement. This should be
* called before ::getJoins(), because it may add joins into the
* statement based on the relationships used in the where clause
*/
protected function getWhereHavingClause($queryset) {
$constraints = $this->compileConstraints($queryset->constraints, $model);
$where = $having = array();
foreach ($constraints as $C) {
if ($C->type == CompiledExpression::TYPE_WHERE)
$where[] = $C;
else
$having[] = $C;
}
if (isset($queryset->extra['where'])) {
foreach ($queryset->extra['where'] as $S) {
$where[] = '('.$S.')';
}
}
$where = ' WHERE '.implode(' AND ', $where);
if ($having)
$having = ' HAVING '.implode(' AND ', $having);
return array($where ?: '', $having ?: '');
}
function compileCount($queryset) {
$model = $queryset->model;
$table = $model::$meta['table'];
list($where, $having) = $this->getWhereHavingClause($queryset);
$joins = $this->getJoins($queryset);
$sql = 'SELECT COUNT(*) AS count FROM '.$this->quote($table).$joins.$where;
$exec = new MysqlExecutor($sql, $this->params);
$row = $exec->getArray();
return $row['count'];
}
function compileSelect($queryset) {
$model = $queryset->model;
// Use an alias for the root model table
$this->joins[''] = array('alias' => ($rootAlias = $this->nextAlias()));
// Compile the WHERE clause
$this->annotations = $queryset->annotations ?: array();
list($where, $having) = $this->getWhereHavingClause($queryset);
// Compile the ORDER BY clause
if (($columns = $queryset->getSortFields()) && !isset($this->options['nosort'])) {
if ($sort instanceof SqlFunction) {
$field = $sort->toSql($this, $model);
}
else {
if ($sort[0] == '-') {
$dir = 'DESC';
$sort = substr($sort, 1);
}
list($field) = $this->getField($sort, $model);
// TODO: Throw exception if $field can be indentified as
// invalid
if ($field instanceof SqlFunction)
$field = $field->toSql($this, $model);
$orders[] = $field.' '.$dir;
}
$sort = ' ORDER BY '.implode(', ', $orders);
}
// Compile the field listing
$table = $this->quote($model::$meta['table']).' '.$rootAlias;
$defer = $queryset->defer ?: array();
// Add local fields first
$model::_inspect();
foreach ($model::$meta['fields'] as $f) {
// Handle deferreds
if (isset($defer[$f]))
continue;
$fields[$rootAlias . '.' . $this->quote($f)] = true;
$fieldMap[] = array($theseFields, $model);
// Add the JOINs to this query
foreach ($queryset->related as $sr) {
// XXX: Sort related by the paths so that the shortest paths
// are resolved first when building out the models.
$full_path = '';
$parts = array();
// Track each model traversal and fetch data for each of the
// models in the path of the related table
foreach (explode('__', $sr) as $field) {
$full_path .= $field;
$parts[] = $field;
list($alias, $fmodel) = $this->getField($full_path, $model,
array('table'=>true, 'model'=>true));
$fmodel::_inspect();
foreach ($fmodel::$meta['fields'] as $f) {
// Handle deferreds
if (isset($defer[$sr . '__' . $f]))
continue;
elseif (isset($fields[$alias.'.'.$this->quote($f)]))
continue;
$fields[$alias . '.' . $this->quote($f)] = true;
if ($theseFields) {
$fieldMap[] = array($theseFields, $fmodel, $parts);
}
$full_path .= '__';
}
}
}
// Support retrieving only a list of values rather than a model
elseif ($queryset->values) {
foreach ($queryset->values as $alias=>$v) {
list($f) = $this->getField($v, $model);
$fields[$f->toSql($this, $model, $alias)] = true;
else {
if (!is_int($alias))
$f .= ' AS '.$this->quote($alias);
// If there are annotations, add in these fields to the
// GROUP BY clause
if ($queryset->annotations)
$group_by[] = $unaliased;
}
// Simple selection from one table
else {
if ($queryset->defer) {
foreach ($model::$meta['fields'] as $f) {
if (isset($queryset->defer[$f]))
continue;
$fields[$rootAlias .'.'. $this->quote($f)] = true;
}
}
else {
$fields[$rootAlias.'.*'] = true;
$fields = array_keys($fields);
// Add in annotations
if ($queryset->annotations) {