Skip to content
Snippets Groups Projects
class.orm.php 94.3 KiB
Newer Older
Jared Hancock's avatar
Jared Hancock committed
<?php
/*********************************************************************
    class.orm.php

    Simple ORM (Object Relational Mapper) for PHP5 based on Django's ORM,
Jared Hancock's avatar
Jared Hancock committed
    except that complex filter operations are not supported. The ORM simply
    supports ANDed filter operations without any GROUP BY support.

    Jared Hancock <jared@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/

class OrmException extends Exception {}
class OrmConfigurationException extends Exception {}
// Database fields/tables do not match codebase
class InconsistentModelException extends OrmException {
    function __construct() {
        // Drop the model cache (just incase)
        ModelMeta::flushModelCache();
        call_user_func_array(array('parent', '__construct'), func_get_args());
    }
}
/**
 * Meta information about a model including edges (relationships), table
 * name, default sorting information, database fields, etc.
 *
 * This class is constructed and built automatically from the model's
 * ::_inspect method using a class's ::$meta array.
 */
class ModelMeta implements ArrayAccess {

    static $base = array(
        'pk' => false,
        'table' => false,
        'defer' => array(),
        'select_related' => array(),
        'view' => false,
        'joins' => array(),
        'foreign_keys' => array(),
    static $model_cache;

    var $model;

    function __construct($model) {
        $this->model = $model;
        // Merge ModelMeta from parent model (if inherited)
        $parent = get_parent_class($this->model);
        if (is_subclass_of($parent, 'VerySimpleModel')) {
            $meta = $parent::getMeta()->extend($model::$meta);
        }
        else {
            $meta = $model::$meta + self::$base;
        }
        if (!$meta['view']) {
            if (!$meta['table'])
                throw new OrmConfigurationException(
                    sprintf(__('%s: Model does not define meta.table'), $this->model));
            elseif (!$meta['pk'])
                throw new OrmConfigurationException(
                    sprintf(__('%s: Model does not define meta.pk'), $this->model));
        }

        // Ensure other supported fields are set and are arrays
        foreach (array('pk', 'ordering', 'defer', 'select_related') as $f) {
            if (!isset($meta[$f]))
                $meta[$f] = array();
            elseif (!is_array($meta[$f]))
                $meta[$f] = array($meta[$f]);
        }

        // Break down foreign-key metadata
        foreach ($meta['joins'] as $field => &$j) {
            $this->processJoin($j);
            if ($j['local'])
                $meta['foreign_keys'][$j['local']] = $field;
        unset($j);
    function extend($meta) {
        if ($meta instanceof self)
            $meta = $meta->base;
        return $meta + $this->base + self::$base;
    }

    /**
     * Adds some more information to a declared relationship. If the
     * relationship is a reverse relation, then the information from the
     * reverse relation is loaded into the local definition
     *
     * Compiled-Join-Structure:
     * 'constraint' => array(local => array(foreign_field, foreign_class)),
     *      Constraint used to construct a JOIN in an SQL query
     * 'list' => boolean
     *      TRUE if an InstrumentedList should be employed to fetch a list
     *      of related items
     * 'broker' => Handler for the 'list' property. Usually a subclass of
     *      'InstrumentedList'
     * 'null' => boolean
     *      TRUE if relation is nullable
     * 'fkey' => array(class, pk)
     *      Classname and field of the first item in the constraint that
     *      points to a PK field of a foreign model
     * 'local' => string
     *      The local field corresponding to the 'fkey' property
     */
    function processJoin(&$j) {
        $constraint = array();
        if (isset($j['reverse'])) {
            list($fmodel, $key) = explode('.', $j['reverse']);
            // NOTE: It's ok if the forein meta data is not yet inspected.
            $info = $fmodel::$meta['joins'][$key];
            if (!is_array($info['constraint']))
                throw new OrmConfigurationException(sprintf(__(
                    // `reverse` here is the reverse of an ORM relationship
                    '%s: Reverse does not specify any constraints'),
                    $j['reverse']));
            foreach ($info['constraint'] as $foreign => $local) {
                list($L,$field) = is_array($local) ? $local : explode('.', $local);
                $constraint[$field ?: $L] = array($fmodel, $foreign);
            }
            if (!isset($j['list']))
                $j['list'] = true;
            if (!isset($j['null']))
                // By default, reverse releationships can be empty lists
                $j['null'] = true;
        else {
            foreach ($j['constraint'] as $local => $foreign) {
                list($class, $field) = $constraint[$local]
                    = is_array($foreign) ? $foreign : explode('.', $foreign);
        if ($j['list'] && !isset($j['broker'])) {
            $j['broker'] = 'InstrumentedList';
        }
        if ($j['broker'] && !class_exists($j['broker'])) {
            throw new OrmException($j['broker'] . ': List broker does not exist');
        }
        foreach ($constraint as $local => $foreign) {
            list($class, $field) = $foreign;
            if ($local[0] == "'" || $field[0] == "'" || !class_exists($class))
                continue;
            $j['fkey'] = $foreign;
            $j['local'] = $local;
        }
        $j['constraint'] = $constraint;
    function addJoin($name, array $join) {
        $this->base['joins'][$name] = $join;
        $this->processJoin($this->base['joins'][$name]);
    }

    function offsetGet($field) {
        if (!isset($this->base[$field]))
            $this->setupLazy($field);
        return $this->base[$field];
    }
    function offsetSet($field, $what) {
        $this->base[$field] = $what;
    }
    function offsetExists($field) {
        return isset($this->base[$field]);
    }
    function offsetUnset($field) {
        throw new Exception('Model MetaData is immutable');
    }

    function setupLazy($what) {
        switch ($what) {
        case 'fields':
            $this->base['fields'] = self::inspectFields();
            break;
        case 'newInstance':
            $class_repr = sprintf(
                'O:%d:"%s":0:{}',
                strlen($this->model), $this->model
            );
            $this->base['newInstance'] = function() use ($class_repr) {
                return unserialize($class_repr);
            };
            break;
        default:
            throw new Exception($what . ': No such meta-data');
        }
    }

    function inspectFields() {
        if (!isset(self::$model_cache))
            self::$model_cache = function_exists('apc_fetch');
        if (self::$model_cache) {
            $key = md5(SECRET_SALT . GIT_VERSION . $this['table']);
            if ($fields = apc_fetch($key)) {
                return $fields;
            }
        }
        $fields = DbEngine::getCompiler()->inspectTable($this['table']);
        if (self::$model_cache) {
            apc_store($key, $fields);
        }
        return $fields;

    static function flushModelCache() {
        if (self::$model_cache)
            @apc_clear_cache('user');
    }
Jared Hancock's avatar
Jared Hancock committed
class VerySimpleModel {
    static $meta = array(
        'table' => false,
        'ordering' => false,
        'pk' => false
    );

    var $ht;
Jared Hancock's avatar
Jared Hancock committed
    var $__new__ = false;
    var $__deleted__ = false;
    var $__deferred__ = array();
Jared Hancock's avatar
Jared Hancock committed

    function __construct($row) {
        $this->ht = $row;
    }

    function get($field, $default=false) {
Jared Hancock's avatar
Jared Hancock committed
        if (array_key_exists($field, $this->ht))
            return $this->ht[$field];
        elseif (($joins = static::getMeta('joins')) && isset($joins[$field])) {
            $j = $joins[$field];
            // Support instrumented lists and such
            if (isset($j['list']) && $j['list']) {
                $class = $j['fkey'][0];
                $fkey = array();
                // Localize the foreign key constraint
                foreach ($j['constraint'] as $local=>$foreign) {
                    list($_klas,$F) = $foreign;
                    $fkey[$F ?: $_klas] = ($local[0] == "'")
                        ? trim($local, "'") : $this->ht[$local];
                }
                $v = $this->ht[$field] = new $j['broker'](
                    // Send Model, [Foriegn-Field => Local-Id]
                    array($class, $fkey)
                );
                return $v;
            }
            // Support relationships
            elseif (isset($j['fkey'])) {
                $criteria = array();
                foreach ($j['constraint'] as $local => $foreign) {
                    list($klas,$F) = $foreign;
                    if (class_exists($klas))
                        $class = $klas;
                    if ($local[0] == "'") {
                        $criteria[$F] = trim($local,"'");
                    }
                    elseif ($F[0] == "'") {
                        // Does not affect the local model
                        continue;
                    }
                    else {
                        $criteria[$F] = $this->ht[$local];
                    }
                }
                    $v = $this->ht[$field] = $class::lookup($criteria);
                }
                catch (DoesNotExist $e) {
                    $v = null;
                }
                return $v;
            }
        elseif (isset($this->__deferred__[$field])) {
            // Fetch deferred field
            $row = static::objects()->filter($this->getPk())
                // XXX: Seems like all the deferred fields should be fetched
                ->values_flat($field)
                ->one();
            if ($row)
                return $this->ht[$field] = $row[0];
        }
        elseif ($field == 'pk') {
            return $this->getPk();
        }

        if (isset($default))
            return $default;

        // For new objects, assume the field is NULLable
        if ($this->__new__)
            return null;

        // Check to see if the column referenced is actually valid
        if (in_array($field, static::getMeta('fields')))
            return null;

        throw new OrmException(sprintf(__('%s: %s: Field not defined'),
            get_class($this), $field));
    }
    function __get($field) {
        return $this->get($field, null);
    }
Jared Hancock's avatar
Jared Hancock committed
    function getByPath($path) {
        if (is_string($path))
            $path = explode('__', $path);
        $root = $this;
        foreach ($path as $P)
            $root = $root->get($P);
        return $root;
    }

    function __isset($field) {
        return array_key_exists($field, $this->ht)
            || isset(static::$meta['joins'][$field]);
        if ($this->__isset($field))
            unset($this->ht[$field]);
        else
            unset($this->{$field});
Jared Hancock's avatar
Jared Hancock committed

    function set($field, $value) {
        // Update of foreign-key by assignment to model instance
        $related = false;
        $joins = static::getMeta('joins');
        if (isset($joins[$field])) {
            $j = $joins[$field];
            if ($j['list'] && ($value instanceof InstrumentedList)) {
                // Magic list property
                $this->ht[$field] = $value;
                return;
            }
            if ($value === null) {
                $this->ht[$field] = $value;
                if (in_array($j['local'], static::$meta['pk'])) {
                    // Reverse relationship — don't null out local PK
                    return;
                }
                // Pass. Set local field to NULL in logic below
            }
            elseif ($value instanceof $j['fkey'][0]) {
                // Capture the object under the object's field name
                $this->ht[$field] = $value;
                if ($value->__new__)
                    // save() will be performed when saving this object
                    $value = null;
                else
                    $value = $value->get($j['fkey'][1]);
                // Fall through to the standard logic below
            }
            else
                throw new InvalidArgumentException(
                    sprintf(__('Expecting NULL or instance of %s. Got a %s instead'),
                    $j['fkey'][0], is_object($value) ? get_class($value) : gettype($value)));
            // Capture the foreign key id value
            $field = $j['local'];
        }
        // elseif $field is in a relationship, adjust the relationship
        elseif (isset(static::$meta['foreign_keys'][$field])) {
            // meta->foreign_keys->{$field} points to the property of the
            // foreign object. For instance 'object_id' points to 'object'
            $related = static::$meta['foreign_keys'][$field];
        }
Jared Hancock's avatar
Jared Hancock committed
        $old = isset($this->ht[$field]) ? $this->ht[$field] : null;
        if ($old != $value) {
            // isset should not be used here, because `null` should not be
            // replaced in the dirty array
            if (!array_key_exists($field, $this->dirty))
                $this->dirty[$field] = $old;
            if ($related)
                // $related points to a foreign object propery. If setting a
                // new object_id value, the relationship to object should be
                // cleared and rebuilt
                unset($this->ht[$related]);
Jared Hancock's avatar
Jared Hancock committed
    }
    function __set($field, $value) {
        return $this->set($field, $value);
    }

    function setAll($props) {
        foreach ($props as $field=>$value)
            $this->set($field, $value);
    }

    function __onload() {}
    static function __oninspect() {}
    static function _inspect() {
        static::$meta = new ModelMeta(get_called_class());
        // Let the model participate
        static::__oninspect();
    }

    static function getMeta($key=false) {
        if (!static::$meta instanceof ModelMeta)
            static::_inspect();
        $M = static::$meta;
        return ($key) ? $M->offsetGet($key) : $M;
    /**
     * objects
     *
     * Retrieve a QuerySet for this model class which can be used to fetch
     * models from the connected database. Subclasses can override this
     * method to apply forced constraints on the QuerySet.
     */
Jared Hancock's avatar
Jared Hancock committed
    static function objects() {
        return new QuerySet(get_called_class());
    }

    /**
     * lookup
     *
     * Retrieve a record by its primary key. This method may be short
     * circuited by model caching if the record has already been loaded by
     * the database. In such a case, the database will not be consulted for
     * the model's data.
     *
     * This method can be called with an array of keyword arguments matching
     * the PK of the object or the values of the primary key. Both of these
     * usages are correct:
     *
     * >>> User::lookup(1)
     * >>> User::lookup(array('id'=>1))
     *
     * For composite primary keys and the first usage, pass the values in
     * the order they are given in the Model's 'pk' declaration in its meta
     * data.
     *
     * Parameters:
     * $criteria - (mixed) primary key for the sought model either as
     *      arguments or key/value array as the function's first argument
     *
     * Returns:
     * (Object<Model>|null) a single instance of the sought model or null if
     * no such instance exists.
Jared Hancock's avatar
Jared Hancock committed
    static function lookup($criteria) {
        // Model::lookup(1), where >1< is the pk value
        if (!is_array($criteria)) {
            $criteria = array();
            $pk = static::getMeta('pk');
            foreach (func_get_args() as $i=>$f)
                $criteria[$pk[$i]] = $f;

            // Only consult cache for PK lookup, which is assumed if the
            // values are passed as args rather than an array
            if ($cached = ModelInstanceManager::checkCache(get_called_class(),
                    $criteria))
                return $cached;
        try {
            return static::objects()->filter($criteria)->one();
        }
        catch (DoesNotExist $e) {
            return null;
        }
        $ex = DbEngine::delete($this);
        try {
            $ex->execute();
            if ($ex->affected_rows() != 1)
                return false;
            $this->__deleted__ = true;
            Signal::send('model.deleted', $this);
        }
        catch (OrmException $e) {
            return false;
        }
Jared Hancock's avatar
Jared Hancock committed
    }

    function save($refetch=false) {
        if ($this->__deleted__)
            throw new OrmException('Trying to update a deleted object');

        $pk = static::getMeta('pk');
        $wasnew = $this->__new__;

        // First, if any foreign properties of this object are connected to
        // another *new* object, then save those objects first and set the
        // local foreign key field values
        foreach (static::getMeta('joins') as $prop => $j) {
            if (isset($this->ht[$prop])
                && ($foreign = $this->ht[$prop])
                && $foreign instanceof VerySimpleModel
                && !in_array($j['local'], $pk)
                && null === $this->get($j['local'])
            ) {
                if ($foreign->__new__ && !$foreign->save())
                    return false;
                $this->set($j['local'], $foreign->get($j['fkey'][1]));
            }
        }

        // If there's nothing in the model to be saved, then we're done
Jared Hancock's avatar
Jared Hancock committed
        if (count($this->dirty) === 0)
            return true;

        $ex = DbEngine::save($this);
        try {
            $ex->execute();
            if ($ex->affected_rows() != 1) {
                // This doesn't really signify an error. It just means that
                // the database believes that the row did not change. For
                // inserts though, it's a deal breaker
                if ($this->__new__)
                    return false;
                else
                    // No need to reload the record if requested — the
                    // database didn't update anything
                    $refetch = false;
            }
        catch (OrmException $e) {
            return false;
Jared Hancock's avatar
Jared Hancock committed
            if (count($pk) == 1)
                // XXX: Ensure AUTO_INCREMENT is set for the field
                $this->ht[$pk[0]] = $ex->insert_id();
Jared Hancock's avatar
Jared Hancock committed
            $this->__new__ = false;
            Signal::send('model.created', $this);
        }
        else {
            $data = array('dirty' => $this->dirty);
            Signal::send('model.updated', $this, $data);
Jared Hancock's avatar
Jared Hancock committed
        }
        # Refetch row from database
        if ($refetch) {
            // Preserve non database information such as list relationships
            // across the refetch
                static::objects()->filter($this->getPk())->values()->one()
                + $this->ht;
        }
        if ($wasnew) {
            // Attempt to update foreign, unsaved objects with the PK of
            // this newly created object
            foreach (static::getMeta('joins') as $prop => $j) {
                if (isset($this->ht[$prop])
                    && ($foreign = $this->ht[$prop])
                    && in_array($j['local'], $pk)
                ) {
                    if ($foreign instanceof VerySimpleModel
                        && null === $foreign->get($j['fkey'][1])
                    ) {
                        $foreign->set($j['fkey'][1], $this->get($j['local']));
                    }
                    elseif ($foreign instanceof InstrumentedList) {
                        foreach ($foreign as $item) {
                            if (null === $item->get($j['fkey'][1]))
                                $item->set($j['fkey'][1], $this->get($j['local']));
                        }
                    }
                }
            }
Jared Hancock's avatar
Jared Hancock committed
        $this->dirty = array();
Jared Hancock's avatar
Jared Hancock committed
    }

    static function create($ht=false) {
        if (!$ht) $ht=array();
        $class = get_called_class();
        $i = new $class(array());
        $i->__new__ = true;
        foreach ($ht as $field=>$value)
            if (!is_array($value))
                $i->set($field, $value);
        return $i;
    }

    private function getPk() {
        $pk = array();
        foreach ($this::getMeta('pk') as $f)
            $pk[$f] = $this->ht[$f];
        return $pk;
    }
/**
 * AnnotatedModel
 *
 * Simple wrapper class which allows wrapping and write-protecting of
 * annotated fields retrieved from the database. Instances of this class
 * will delegate most all of the heavy lifting to the wrapped Model instance.
 */
class AnnotatedModel {

    var $model;
    var $annotations;

    function __construct($model, $annotations) {
        $this->model = $model;
        $this->annotations = $annotations;
    }

    function __get($what) {
        return $this->get($what);
    }
    function get($what) {
        if (isset($this->annotations[$what]))
            return $this->annotations[$what];
        return $this->model->get($what, null);
    }
    function __set($what, $to) {
        return $this->set($what, $to);
    }
    function set($what, $to) {
        if (isset($this->annotations[$what]))
            throw new OrmException('Annotated fields are read-only');
        return $this->model->set($what, $to);
    }

    function __isset($what) {
        return isset($this->annotations[$what]) || $this->model->__isset($what);
    }

    // Delegate everything else to the model
    function __call($what, $how) {
        return call_user_func_array(array($this->model, $what), $how);
    }
}

Jared Hancock's avatar
Jared Hancock committed
class SqlFunction {
Jared Hancock's avatar
Jared Hancock committed
    function SqlFunction($name) {
        $this->func = $name;
        $this->args = array_slice(func_get_args(), 1);
    }

    function input($what, $compiler, $model) {
        if ($what instanceof SqlFunction)
            $A = $what->toSql($compiler, $model);
        elseif ($what instanceof Q)
            $A = $compiler->compileQ($what, $model);
        else
            $A = $compiler->input($what);
        return $A;
    }

    function toSql($compiler, $model=false, $alias=false) {
        $args = array();
        foreach ($this->args as $A) {
            $args[] = $this->input($A, $compiler, $model);
        }
        return sprintf('%s(%s)%s', $this->func, implode(',', $args),
            $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
    }

    function getAlias() {
        return $this->alias;
    }
    function setAlias($alias) {
        $this->alias = $alias;

    static function __callStatic($func, $args) {
        $I = new static($func);
        $I->args = $args;
        return $I;
    }
class SqlCase extends SqlFunction {
    var $cases = array();
    var $else = false;

    static function N() {
        return new static('CASE');
    }

    function when($expr, $result) {
        if (is_array($expr))
            $expr = new Q($expr);
        $this->cases[] = array($expr, $result);
        return $this;
    }
    function otherwise($result) {
        $this->else = $result;
        return $this;
    }

    function toSql($compiler, $model=false, $alias=false) {
        $cases = array();
        foreach ($this->cases as $A) {
            list($expr, $result) = $A;
            $expr = $this->input($expr, $compiler, $model);
            $result = $this->input($result, $compiler, $model);
            $cases[] = "WHEN {$expr} THEN {$result}";
        }
        if ($this->else) {
            $else = $this->input($this->else, $compiler, $model);
            $cases[] = "ELSE {$else}";
        }
        return sprintf('CASE %s END%s', implode(' ', $cases),
            $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
    }
}

class SqlExpr extends SqlFunction {
    function __construct($args) {
        $this->args = $args;
    }

    function toSql($compiler, $model=false, $alias=false) {
        $O = array();
        foreach ($this->args as $field=>$value) {
            list($field, $op) = $compiler->getField($field, $model);
            if (is_callable($op))
                $O[] = call_user_func($op, $field, $value, $model);
            else
                $O[] = sprintf($op, $field, $compiler->input($value));
        }
        return implode(' ', $O) . ($alias ? ' AS ' . $alias : '');
    }
}

class SqlExpression extends SqlFunction {
    var $operator;
    var $operands;

    function toSql($compiler, $model=false, $alias=false) {
        $O = array();
        foreach ($this->args as $operand) {
            if ($operand instanceof SqlFunction)
                $O[] = $operand->toSql($compiler, $model);
            else
                $O[] = $compiler->input($operand);
        }
        return implode(' '.$this->func.' ', $O)
            . ($alias ? ' AS '.$compiler->quote($alias) : '');
    }

    static function __callStatic($operator, $operands) {
        switch ($operator) {
            case 'minus':
                $operator = '-'; break;
            case 'plus':
                $operator = '+'; break;
            case 'times':
                $operator = '*'; break;
            case 'bitand':
                $operator = '&'; break;
            case 'bitor':
                $operator = '|'; break;
            default:
                throw new InvalidArgumentException('Invalid operator specified');
        }
        return parent::__callStatic($operator, $operands);
    }
}

class SqlInterval extends SqlFunction {
    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 SqlFunction {
    var $level;

    function __construct($field, $level=0) {
        $this->field = $field;
        $this->level = $level;
    }

    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;
    }
}

class SqlAggregate extends SqlFunction {

    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, $alias);
        }
        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
                if (count($fpk) == 1) {
                    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 {
Jared Hancock's avatar
Jared Hancock committed
    var $model;

    var $constraints = array();
    var $path_constraints = array();
Jared Hancock's avatar
Jared Hancock committed
    var $ordering = array();
    var $limit = false;
    var $offset = 0;
    var $related = array();
    var $values = array();
    var $aggregated = false;
    var $annotations = array();
    var $extra = array();
    var $distinct = array();
    var $lock = false;

    const LOCK_EXCLUSIVE = 1;
    const LOCK_SHARED = 2;
    const ASC = 'ASC';
    const DESC = 'DESC';

Jared Hancock's avatar
Jared Hancock committed
    var $compiler = 'MySqlCompiler';
    var $iterator = 'ModelInstanceManager';
Jared Hancock's avatar
Jared Hancock committed

    var $query;
Jared Hancock's avatar
Jared Hancock committed

    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);
Jared Hancock's avatar
Jared Hancock committed
        return $this;
    }

    function exclude() {
        foreach (func_get_args() as $Q) {
            $this->constraints[] = $Q instanceof Q ? $Q->negate() : Q::not($Q);
Jared Hancock's avatar
Jared Hancock committed
        return $this;
    }

    /**
     * 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);
    function defer() {
        foreach (func_get_args() as $f)
            $this->defer[$f] = true;
        return $this;
    }
    function order_by($order, $direction=false) {
        $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);
Jared Hancock's avatar
Jared Hancock committed
        return $this;
    }
    function getSortFields() {
        $ordering = $this->ordering;