Skip to content
Snippets Groups Projects
class.orm.php 83.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jared Hancock's avatar
    Jared Hancock committed
        function count() {
    
            // Defer to the iterator if fetching already started
            if (isset($this->_iterator)) {
                return $this->_iterator->count();
            }
            elseif (isset($this->_count)) {
                return $this->_count;
            }
    
            $class = $this->compiler;
            $compiler = new $class();
    
            return $this->_count = $compiler->compileCount($this);
    
        /**
         * exists
         *
         * Determines if there are any rows in this QuerySet. This can be
         * achieved either by evaluating a SELECT COUNT(*) query or by
         * attempting to fetch the first row from the recordset and return
         * boolean success.
         *
         * Parameters:
         * $fetch - (bool) TRUE if a compile and fetch should be attempted
         *      instead of a SELECT COUNT(*). This would be recommended if an
         *      accurate count is not required and the records would be fetched
         *      if this method returns TRUE.
         *
         * Returns:
         * (bool) TRUE if there would be at least one record in this QuerySet
         */
        function exists($fetch=false) {
            if ($fetch) {
                return (bool) $this[0];
            }
            return $this->count() > 0;
        }
    
    
        function annotate($annotations) {
            if (!is_array($annotations))
                $annotations = func_get_args();
            foreach ($annotations as $name=>$A) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                if ($A instanceof SqlAggregate) {
    
                    if (is_int($name))
                        $name = $A->getFieldName();
                    $A->setAlias($name);
                }
    
    Peter Rotich's avatar
    Peter Rotich committed
                $this->annotations[$name] = $A;
    
        function delete() {
            $class = $this->compiler;
            $compiler = new $class();
    
            // XXX: Mark all in-memory cached objects as deleted
    
            $ex = $compiler->compileBulkDelete($this);
    
            $ex->execute();
            return $ex->affected_rows();
        }
    
    
        function update(array $what) {
            $class = $this->compiler;
            $compiler = new $class;
            $ex = $compiler->compileBulkUpdate($this, $what);
            $ex->execute();
            return $ex->affected_rows();
        }
    
        function __clone() {
            unset($this->_iterator);
            unset($this->query);
        }
    
    
    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;
    
            $query = clone $this;
            if (!$query->ordering && isset($model::$meta['ordering']))
                $query->ordering = $model::$meta['ordering'];
    
            if (false !== $query->related && !$query->values && $model::$meta['select_related'])
    
                $query->related = $model::$meta['select_related'];
            if (!$query->defer && $model::$meta['defer'])
                $query->defer = $model::$meta['defer'];
    
            $class = $this->compiler;
            $compiler = new $class($options);
    
            $this->query = $compiler->compileSelect($query);
    
    Jared Hancock's avatar
    Jared Hancock committed
    
            return $this->query;
        }
    
        /**
         * Fetch a model class which can be used to render the QuerySet as a
         * subquery to be used as a JOIN.
         */
    
        function asView() {
            $unique = spl_object_hash($this);
            $classname = "QueryView{$unique}";
            $class = <<<EOF
    class {$classname} extends VerySimpleModel {
        static \$meta = array(
            'view' => true,
        );
        static \$queryset;
    
        static function getQuery(\$compiler) {
            return ' ('.static::\$queryset->getQuery().') ';
        }
    }
    EOF;
            eval($class); // Ugh
            $classname::$queryset = $this;
            return $classname;
        }
    
    
        function serialize() {
            $info = get_object_vars($this);
            unset($info['query']);
            unset($info['limit']);
            unset($info['offset']);
            unset($info['_iterator']);
            return serialize($info);
        }
    
        function unserialize($data) {
            $data = unserialize($data);
            foreach ($data as $name => $val) {
                $this->{$name} = $val;
            }
        }
    
    class DoesNotExist extends Exception {}
    class ObjectNotUnique extends Exception {}
    
    
    abstract class ResultSet implements Iterator, ArrayAccess, Countable {
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $resource;
        var $position = 0;
        var $queryset;
    
        var $cache = array();
    
        function __construct($queryset=false) {
    
            if ($queryset) {
                $this->model = $queryset->model;
                $this->resource = $queryset->getQuery();
    
            }
        }
    
        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();
    
        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) {
    
            $key = $modelClass;
    
            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.
         */
    
    Peter Rotich's avatar
    Peter Rotich committed
        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) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                foreach ($annotations as $name=>$A) {
    
                    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.
    
    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($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);
    
                    elseif ($model) {
    
                        $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));
    
                    $offset += count($fields);
    
                $model = $this->getOrBuild($this->model, $row);
    
            return $model;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function fillTo($index) {
    
            $func = ($this->map) ? 'getRow' : 'getArray';
    
    Jared Hancock's avatar
    Jared Hancock committed
            while ($this->resource && $index >= count($this->cache)) {
    
                if ($row = $this->resource->{$func}()) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                    $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()) {
    
                    $this->cache[] = $row;
    
                } 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) = $fkey;
    
                $queryset = $model::objects()->filter($this->key);
    
            parent::__construct($queryset);
    
            $this->model = $model;
    
        function add($object, $at=false) {
    
            if (!$object || !$object instanceof $this->model)
    
                throw new Exception(sprintf(
                    'Attempting to add invalid object to list. Expected <%s>, but got <%s>',
                    $this->model,
                    get_class($object)
                ));
    
            foreach ($this->key as $field=>$value)
                $object->set($field, $value);
    
            if (!$object->__new__)
                $object->save();
    
    
            if ($at !== false)
                $this->cache[$at] = $object;
            else
                $this->cache[] = $object;
    
        function remove($object, $delete=true) {
            if ($delete)
                $object->delete();
            else
                $object->set($this->key, null);
    
        function reset() {
            $this->cache = array();
        }
    
    
        /**
         * Reduce the list to a subset using a simply key/value constraint. New
         * items added to the subset will have the constraint automatically
         * added to all new items.
         */
        function window($constraint) {
            $model = $this->model;
            $meta = $model::$meta;
            $key = $this->key;
            foreach ($constraint as $field=>$value) {
                if (!is_string($field) || false === in_array($field, $meta['fields']))
                    throw new OrmException('InstrumentedList windowing must be performed on local fields only');
                $key[$field] = $value;
            }
            return new static(array($this->model, $key), $this->filter($constraint));
        }
    
    
        // QuerySet delegates
        function count() {
    
            return $this->objects()->count();
    
        }
        function exists() {
            return $this->queryset->exists();
        }
        function expunge() {
    
            if ($this->queryset->delete())
                $this->reset();
    
        function update(array $what) {
            return $this->queryset->update($what);
        }
    
    
        // Fetch a new QuerySet
        function objects() {
            return clone $this->queryset;
    
        function offsetUnset($a) {
            $this->fillTo($a);
            $this->cache[$a]->delete();
        }
        function offsetSet($a, $b) {
            $this->fillTo($a);
            $this->cache[$a]->delete();
    
    
        // QuerySet overriedes
    
        function __call($what, $how) {
            return call_user_func_array(array($this->objects(), $what), $how);
    
    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.
    
         *
         * 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()) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            // 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 (isset(static::$operators[$field])) {
    
                    $operator = static::$operators[$field];
                    $field = array_pop($parts);
                }
    
            $path = array();
    
            // Call pushJoin for each segment in the join path. A new JOIN
            // fragment will need to be emitted and/or cached
    
            $joins = array();
            $push = function($p, $path, $model) use (&$joins) {
    
                $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);
    
                $tip = ($crumb) ? "{$crumb}__{$p}" : $p;
                $joins[] = array($crumb, $tip, $model, $info);
    
                // Roll to foreign model
                return $info['fkey'];
    
            foreach ($parts as $p) {
                list($model) = $push($p, $path, $model);
    
                $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($model, $field) = $push($field, $path, $model);
            }
    
            // Add the conststraint as the last arg to the last join
            if (isset($options['constraint'])) {
                $joins[count($joins)-1][] = $options['constraint'];
            }
    
            // Apply the joins list to $this->pushJoin
            foreach ($joins as $A) {
                $alias = call_user_func_array(array($this, 'pushJoin'), $A);
            }
    
            if (!isset($alias)) {
                // Determine the alias for the root model table
                $alias = (isset($this->joins['']))
                    ? $this->joins['']['alias']
                    : $this->quote($rootModel::$meta['table']);
    
    Jared Hancock's avatar
    Jared Hancock committed
            if (isset($options['table']) && $options['table'])
    
                $field = $alias;
    
            elseif (isset($this->annotations[$field]))
                $field = $this->annotations[$field];
    
            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, $constraint=false) {
    
            // TODO: Build the join statement fragment and return the table
            // alias. The table alias will be useful where the join is used in
            // the WHERE and ORDER BY clauses
    
            // If the join already exists for the statement-being-compiled, just
            // return the alias being used.
    
            if (!$constraint && isset($this->joins[$path]))
    
                return $this->joins[$path]['alias'];
    
            // TODO: Support only using aliases if necessary. Use actual table
            // names for everything except oddities like self-joins
    
            $alias = $this->nextAlias();
            // Keep an association between the table alias and the model. This
            // will make model construction much easier when we have the data
            // and the table alias from the database.
            $this->aliases[$alias] = $model;
    
            // TODO: Stash joins and join constraints into local ->joins array.
            // This will be useful metadata in the executor to construct the
            // final models for fetching
            // TODO: Always use a table alias. This will further help with
            // coordination between the data returned from the database (where
            // table alias is available) and the corresponding data.
    
            $T = array('alias' => $alias);
    
            $this->joins[$path] = $T;
            $this->joins[$path]['sql'] = $this->compileJoin($tip, $model, $alias, $info, $constraint);
    
        /**
         * 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) {
    
            $filter = array();
    
            $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
    
                    elseif (is_callable($op))
    
                        $filter[] = call_user_func($op, $field, $value, $model);
    
    Jared Hancock's avatar
    Jared Hancock committed
                    else
    
                        $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);
            }
    
            return $constraints;
    
        }
    
        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) {
    
            ModelInstanceManager::uncache($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'),
    
            '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'),
    
            'like' => '%1$s LIKE %2$s',
    
            'hasbit' => '%1$s & %2$s != 0',
    
            'in' => array('self', '__in'),
    
            '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
    
            // the query as a JOIN and add the join constraint into the WHERE
            // clause.
    
            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);
            }
    
    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 __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) = $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, '\'"'))
    
                // Support local constraint
                // field_name => "'constant'"
    
                elseif ($rmodel[0] == "'" && !$right) {
    
                    $constraints[] = sprintf("%s.%s = %s",
                        $table, $this->quote($local),
    
                        $this->input(trim($rmodel, '\'"'))
    
                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);
    
            if (!isset($rmodel))
                $rmodel = $model;
    
            // Support inline views
            $table = ($rmodel::$meta['view'])
    
                // XXX: Support parameters from the nested query
    
                ? $rmodel::getQuery($this)
                : $this->quote($rmodel::$meta['table']);
    
            $base = "{$join}{$table} {$alias}";
    
               $base .= ' ON ('.implode(' AND ', $constraints).')';
    
        /**
         * input
         *
         * Generate a parameterized input for a database query. Input value is
         * received by reference to avoid copying.
         *
         * Parameters: