Skip to content
Snippets Groups Projects
class.orm.php 44.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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 {}
    
    /**
     * 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 {
    
        var $base = array();
    
        function __construct($model) {
            $meta = $model::$meta;
    
            if (!$meta['table'])
                throw new OrmConfigurationException(
                    __('Model does not define meta.table'), $model);
            elseif (!$meta['pk'])
                throw new OrmConfigurationException(
                    __('Model does not define meta.pk'), $model);
    
            // Ensure other supported fields are set and are arrays
            foreach (array('pk', 'ordering', 'deferred') as $f) {
                if (!isset($meta[$f]))
                    $meta[$f] = array();
                elseif (!is_array($meta[$f]))
                    $meta[$f] = array($meta[$f]);
            }
    
            // Break down foreign-key metadata
    
            if (!isset($meta['joins']))
                $meta['joins'] = array();
    
            foreach ($meta['joins'] as $field => &$j) {
                if (isset($j['reverse'])) {
                    list($model, $key) = explode('.', $j['reverse']);
                    $info = $model::$meta['joins'][$key];
                    $constraint = array();
                    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(,$field) = explode('.', $local);
                        $constraint[$field] = "$model.$foreign";
                    }
                    $j['constraint'] = $constraint;
                    if (!isset($j['list']))
                        $j['list'] = true;
                }
                // XXX: Make this better (ie. composite keys)
                $keys = array_keys($j['constraint']);
                $foreign = $j['constraint'][$keys[0]];
                $j['fkey'] = explode('.', $foreign);
                $j['local'] = $keys[0];
            }
            $this->base = $meta;
        }
    
        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;
            default:
                throw new Exception($what . ': No such meta-data');
            }
        }
    
        function inspectFields() {
            return DbEngine::getCompiler()->inspectTable($this['table']);
        }
    }
    
    
    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 $__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 (isset(static::$meta['joins'][$field])) {
    
                // Make sure joins were inspected
    
                if (!static::$meta instanceof ModelMeta)
                    static::_inspect();
                $j = static::$meta['joins'][$field];
                // Support instrumented lists and such
                if (isset($this->ht[$j['local']])
                        && isset($j['list']) && $j['list']) {
                    $fkey = $j['fkey'];
                    $v = $this->ht[$name] = new InstrumentedList(
                        // Send Model, Foriegn-Field, Local-Id
                        array($fkey[0], $fkey[1], $this->get($j['local']))
                    );
                    return $v;
                }
                // Support relationships
                elseif (isset($j['fkey'])
    
                        && ($class = $j['fkey'][0])
                        && class_exists($class)) {
                    $v = $this->ht[$field] = $class::lookup(
                        array($j['fkey'][1] => $this->ht[$j['local']]));
                    return $v;
                }
    
            elseif (isset($this->__deferred__[$field])) {
                // Fetch deferred field
                $row = static::objects()->filter($this->getPk())
                    ->values_flat($field)
                    ->one();
                if ($row)
                    return $this->ht[$field] = $row[0];
            }
            elseif ($field == 'pk') {
                return $this->getPk();
            }
    
    
            if (isset($default))
                return $default;
    
            // TODO: Inspect fields from database before throwing this error
    
            throw new OrmException(sprintf(__('%s: %s: Field not defined'),
    
                get_class($this), $field));
        }
    
        function __get($field) {
            return $this->get($field, null);
        }
    
    
        function __isset($field) {
            return array_key_exists($field, $this->ht)
                || isset(static::$meta['joins'][$field]);
    
        function __unset($field) {
            unset($this->ht[$field]);
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function set($field, $value) {
    
            // Update of foreign-key by assignment to model instance
            if (isset(static::$meta['joins'][$field])) {
                $j = static::$meta['joins'][$field];
    
                if ($j['list'] && ($value instanceof InstrumentedList)) {
                    // Magic list property
                    $this->ht[$field] = $value;
                    return;
                }
    
                if ($value === null) {
                    // Pass. Set local field to NULL in logic below
                }
                elseif ($value instanceof $j['fkey'][0]) {
                    if ($value->__new__)
                        $value->save();
                    // Capture the object under the object's field name
                    $this->ht[$field] = $value;
                    $value = $value->get($j['fkey'][1]);
                    // Fall through to the standard logic below
                }
                else
                    throw new InvalidArgumentException(
    
                        sprintf(__('Expecting NULL or instance of %s'), $j['fkey'][0]));
    
                // Capture the foreign key id value
                $field = $j['local'];
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
            // XXX: Fully support or die if updating pk
            // XXX: The contents of $this->dirty should be the value after the
            // previous fetch or save. For instance, if the value is changed more
            // than once, the original value should be preserved in the dirty list
            // on the second edit.
            $old = isset($this->ht[$field]) ? $this->ht[$field] : null;
            if ($old != $value) {
                $this->dirty[$field] = $old;
                $this->ht[$field] = $value;
            }
        }
        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() {
    
            if (!static::$meta instanceof ModelMeta) {
                static::$meta = new ModelMeta(get_called_class());
    
                // Let the model participate
                static::__oninspect();
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        static function objects() {
            return new QuerySet(get_called_class());
        }
    
        static function lookup($criteria) {
            if (!is_array($criteria))
                // Model::lookup(1), where >1< is the pk value
                $criteria = array(static::$meta['pk'][0] => $criteria);
    
            return static::objects()->filter($criteria)->one();
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function delete($pk=false) {
    
            $ex = DbEngine::delete($this);
            try {
                $ex->execute();
                if ($ex->affected_rows() != 1)
                    return false;
    
                Signal::send('model.deleted', $this);
            }
            catch (OrmException $e) {
                return false;
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function save($refetch=false) {
            if (count($this->dirty) === 0)
    
                return true;
    
    
            $ex = DbEngine::save($this);
            try {
                $ex->execute();
                if ($ex->affected_rows() != 1)
                    return false;
    
            catch (OrmException $e) {
                return false;
    
    
            $pk = static::$meta['pk'];
    
    
    Jared Hancock's avatar
    Jared Hancock committed
            if ($this->__new__) {
                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
            # XXX: Too much voodoo
            if ($refetch) {
                # XXX: Support composite PK
                $criteria = array($pk[0] => $this->get($pk[0]));
                $self = static::lookup($criteria);
                $this->ht = $self->ht;
            }
            $this->dirty = array();
            return $this->get($pk[0]);
        }
    
        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::$meta['pk'] as $f)
                $pk[$f] = $this->ht[$f];
            return $pk;
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class SqlFunction {
        function SqlFunction($name) {
            $this->func = $name;
            $this->args = array_slice(func_get_args(), 1);
        }
    
    
        function toSql($compiler=false) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            $args = (count($this->args)) ? implode(',', db_input($this->args)) : "";
            return sprintf('%s(%s)', $this->func, $args);
        }
    
    
        static function __callStatic($func, $args) {
            $I = new static($func);
            $I->args = $args;
            return $I;
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class QuerySet implements IteratorAggregate, ArrayAccess {
        var $model;
    
        var $constraints = array();
        var $ordering = array();
        var $limit = false;
        var $offset = 0;
        var $related = array();
        var $values = array();
    
        var $lock = false;
    
        const LOCK_EXCLUSIVE = 1;
        const LOCK_SHARED = 2;
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        var $compiler = 'MySqlCompiler';
        var $iterator = 'ModelInstanceIterator';
    
        var $params;
        var $query;
    
        function __construct($model) {
            $this->model = $model;
        }
    
        function filter() {
            // Multiple arrays passes means OR
    
            $filter = array();
            foreach (func_get_args() as $Q) {
                $filter[] = $Q instanceof Q ? $Q : new Q($Q);
            }
            $this->constraints[] = new Q($filter, Q::ANY);
    
    Jared Hancock's avatar
    Jared Hancock committed
            return $this;
        }
    
        function exclude() {
    
            $filter = array();
            foreach (func_get_args() as $Q) {
                $filter[] = $Q instanceof Q ? $Q->negate() : Q::not($Q);
            }
            $this->constraints[] = new Q($filter, Q::ANY);
    
    Jared Hancock's avatar
    Jared Hancock committed
            return $this;
        }
    
    
        function defer() {
            foreach (func_get_args() as $f)
                $this->defer[$f] = true;
            return $this;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function order_by() {
            $this->ordering = array_merge($this->ordering, func_get_args());
            return $this;
        }
    
    
        function lock($how=false) {
            $this->lock = $how ?: self::LOCK_EXCLUSIVE;
            return $this;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function limit($count) {
            $this->limit = $count;
            return $this;
        }
    
        function offset($at) {
            $this->offset = $at;
            return $this;
        }
    
        function select_related() {
            $this->related = array_merge($this->related, func_get_args());
            return $this;
        }
    
        function values() {
            $this->values = func_get_args();
            $this->iterator = 'HashArrayIterator';
            return $this;
        }
    
    
        function values_flat() {
            $this->values = func_get_args();
            $this->iterator = 'FlatArrayIterator';
            return $this;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function all() {
            return $this->getIterator()->asArray();
        }
    
    
        function one() {
    
            $list = $this->limit(1)->all();
            // TODO: Throw error if more than one result from database
    
            return $this[0];
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function count() {
    
            $class = $this->compiler;
            $compiler = new $class();
    
    Jared Hancock's avatar
    Jared Hancock committed
            return $compiler->compileCount($this);
        }
    
    
        function exists() {
            return $this->count() > 0;
        }
    
    
        function delete() {
            $class = $this->compiler;
            $compiler = new $class();
    
            $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);
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        // IteratorAggregate interface
        function getIterator() {
    
            $class = $this->iterator;
    
    Jared Hancock's avatar
    Jared Hancock committed
            if (!isset($this->_iterator))
    
                $this->_iterator = new $class($this);
    
    Jared Hancock's avatar
    Jared Hancock committed
            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'));
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
        function offsetSet($a, $b) {
    
            throw new Exception(__('QuerySet is read-only'));
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function __toString() {
    
            return (string) $this->getQuery();
    
        function getQuery($options=array()) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            if (isset($this->query))
                return $this->query;
    
            // Load defaults from model
            $model = $this->model;
            if (!$this->ordering && isset($model::$meta['ordering']))
                $this->ordering = $model::$meta['ordering'];
    
    
            $class = $this->compiler;
            $compiler = new $class($options);
    
    Jared Hancock's avatar
    Jared Hancock committed
            $this->query = $compiler->compileSelect($this);
    
            return $this->query;
        }
    }
    
    class ModelInstanceIterator implements Iterator, ArrayAccess {
        var $model;
        var $resource;
        var $cache = array();
        var $position = 0;
        var $queryset;
    
        function __construct($queryset=false) {
    
            if ($queryset) {
                $this->model = $queryset->model;
                $this->resource = $queryset->getQuery();
    
                $this->map = $this->resource->getMap();
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        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($count, $model_class, $path, $alias) = $info;
                    $fields = array_slice($row, $offset, $count);
                    if (!$path) {
                        // Build the root model
                        $model = new $this->model($fields); # nolint
                        $model->__onload();
                    }
                    else {
                        foreach ($fields as $name=>$val) {
                            $fields[substr($name, strlen($alias)+1)] = $val;
                            unset($fields[$name]);
                        }
                        // Link the related model
                        $tail = array_pop($path);
                        $m = $model;
                        foreach ($path as $field) {
                            $m = $m->get($field);
                        }
                        $m->set($tail, new $model_class($fields));
                    }
                    $offset += $count;
                }
            }
            else {
                $model = new $this->model($row); # nolint
                $model->__deferred__ = $this->queryset->defer;
                $model->__onload();
            }
    
            return $model;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function fillTo($index) {
            while ($this->resource && $index >= count($this->cache)) {
                if ($row = $this->resource->getArray()) {
                    $this->cache[] = $this->buildModel($row);
                } else {
                    $this->resource->close();
                    $this->resource = null;
                    break;
                }
            }
        }
    
        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)));
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
        function offsetSet($a, $b) {
    
            throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
    
    class FlatArrayIterator extends ModelInstanceIterator {
        function __construct($queryset) {
            $this->resource = $queryset->getQuery();
        }
        function fillTo($index) {
            while ($this->resource && $index >= count($this->cache)) {
                if ($row = $this->resource->getRow()) {
    
                    $this->cache[] = $row;
    
                } else {
                    $this->resource->close();
                    $this->resource = null;
                    break;
                }
            }
        }
    }
    
    class InstrumentedList extends ModelInstanceIterator {
        var $key;
        var $id;
    
    
        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);
    
            $this->model = $model;
    
            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);
    
            $object->save();
    
    
            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();
    
    class SqlCompiler {
    
        var $options = array();
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $params = array();
    
        var $joins = array();
        var $aliases = array();
        var $alias_num = 1;
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        static $operators = array(
    
            'exact' => '%$1s = %$2s'
    
        function __construct($options=false) {
            if ($options)
                $this->options = array_merge($this->options, $options);
        }
    
    
        /**
         * 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.
         */
        function getField($field, $model, $options=array()) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            $joins = 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'])) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                $field = array_pop($parts);
    
                if (array_key_exists($field, static::$operators)) {
                    $operator = static::$operators[$field];
                    $field = array_pop($parts);
                }
    
            $path = array();
            $crumb = '';
    
            $alias = (isset($this->joins['']))
                ? $this->joins['']['alias']
                : $this->quote($model::$meta['table']);
    
    Jared Hancock's avatar
    Jared Hancock committed
    
            // Traverse through the parts and establish joins between the tables
            // if the field is joined to a foreign model
            if (count($parts) && isset($model::$meta['joins'][$parts[0]])) {
    
                // Call pushJoin for each segment in the join path. A new
                // JOIN fragment will need to be emitted and/or cached
    
    Jared Hancock's avatar
    Jared Hancock committed
                foreach ($parts as $p) {
    
                    $path[] = $p;
                    $tip = implode('__', $path);
    
                    if (!($info = $model::$meta['joins'][$p])) {
                        throw new OrmException(sprintf(
                           'Model `%s` does not have a relation called `%s`',
                            $model, $p));
                    }
    
                    $alias = $this->pushJoin($crumb, $tip, $model, $info);
                    // Roll to foreign model
    
    Jared Hancock's avatar
    Jared Hancock committed
                    foreach ($info['constraint'] as $local => $foreign) {
    
                        list($model, $f) = explode('.', $foreign);
                        if (class_exists($model))
                            break;
    
                    $crumb = $tip;
    
    Jared Hancock's avatar
    Jared Hancock committed
                }
            }
            if (isset($options['table']) && $options['table'])
    
                $field = $alias;
            elseif ($alias)
                $field = $alias.'.'.$this->quote($field);
    
    Jared Hancock's avatar
    Jared Hancock committed
            else
                $field = $this->quote($field);
    
            if (isset($options['model']) && $options['model'])
                $operator = $model;
    
            return array($field, $operator);
    
        /**
         * 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) {
            // 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 (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.
            $this->joins[$path] = array(
                'alias' => $alias,
                'sql'=> $this->compileJoin($tip, $model, $alias, $info),
            );
            return $alias;
        }
    
    
        function compileQ(Q $Q, $model) {
            $filter = array();
            foreach ($Q->constraints as $field=>$value) {
                if ($value instanceof Q) {
                    $filter[] = $this->compileQ($value, $model);
                }
                else {
    
                    list($field, $op) = $this->getField($field, $model);
    
                    if ($value === null)
                        $filter[] = sprintf('%s IS NULL', $field);
    
                    // Allow operators to be callable rather than sprintf
                    // strings
    
                    elseif (is_callable($op))
    
                        $filter[] = call_user_func($op, $field, $value);
    
    Jared Hancock's avatar
    Jared Hancock committed
                    else
                        $filter[] = sprintf($op, $field, $this->input($value));
                }
            }
    
            $glue = $Q->isOred() ? ' OR ' : ' AND ';
            $clause = implode($glue, $filter);
            if (count($filter) > 1)
                $clause = '(' . $clause . ')';
            if ($Q->isNegated())
                $clause = ' NOT '.$clause;
            return $clause;
        }
    
        function compileConstraints($where, $model) {
            $constraints = array();
            foreach ($where as $Q) {
                $constraints[] = $this->compileQ($Q, $model);
            }
            return implode(' AND ', $constraints);
    
        }
    
        function getParams() {
            return $this->params;
        }
    
        function getJoins() {
            $sql = '';
            foreach ($this->joins as $j)
                $sql .= $j['sql'];
            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 DbEngine {
    
    
        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 {
    
        static $operators = array(
            'exact' => '%1$s = %2$s',
            'contains' => array('self', '__contains'),
            'gt' => '%1$s > %2$s',
            'lt' => '%1$s < %2$s',
    
            'gte' => '%1$s >= %2$s',
            'lte' => '%1$s <= %2$s',
    
            'isnull' => array('self', '__isnull'),
    
            'like' => '%1$s LIKE %2$s',
    
            'hasbit' => '%1$s & %2$s != 0',
    
            'in' => array('self', '__in'),
        );
    
        function __contains($a, $b) {
            # {%a} like %{$b}%
    
            return sprintf('%s LIKE %s', $a, $this->input($b = "%$b%"));
    
        }
    
        function __in($a, $b) {
            if (is_array($b)) {
                $vals = array_map(array($this, 'input'), $b);
                $b = implode(', ', $vals);
            }
            else {
                $b = $this->input($b);
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
            return sprintf('%s IN (%s)', $a, $b);
    
        function __isnull($a, $b) {
            return $b
                ? sprintf('%s IS NULL', $a)
                : sprintf('%s IS NOT NULL', $a);
        }
    
    
        function compileJoin($tip, $model, $alias, $info) {
            $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