1<?php
2/*********************************************************************
3    class.orm.php
4
5    Simple ORM (Object Relational Mapper) for PHP5 based on Django's ORM,
6    except that complex filter operations are not supported. The ORM simply
7    supports ANDed filter operations without any GROUP BY support.
8
9    Jared Hancock <jared@osticket.com>
10    Copyright (c)  2006-2013 osTicket
11    http://www.osticket.com
12
13    Released under the GNU General Public License WITHOUT ANY WARRANTY.
14    See LICENSE.TXT for details.
15
16    vim: expandtab sw=4 ts=4 sts=4:
17**********************************************************************/
18require_once INCLUDE_DIR . 'class.util.php';
19
20class OrmException extends Exception {}
21class OrmConfigurationException extends Exception {}
22// Database fields/tables do not match codebase
23class InconsistentModelException extends OrmException {
24    function __construct() {
25        // Drop the model cache (just incase)
26        ModelMeta::flushModelCache();
27        call_user_func_array(array('parent', '__construct'), func_get_args());
28    }
29}
30
31/**
32 * Meta information about a model including edges (relationships), table
33 * name, default sorting information, database fields, etc.
34 *
35 * This class is constructed and built automatically from the model's
36 * ::getMeta() method using a class's ::$meta array.
37 */
38class ModelMeta implements ArrayAccess {
39
40    static $base = array(
41        'pk' => false,
42        'table' => false,
43        'defer' => array(),
44        'select_related' => array(),
45        'view' => false,
46        'joins' => array(),
47        'foreign_keys' => array(),
48    );
49    static $model_cache;
50
51    var $model;
52    var $meta = array();
53    var $new;
54    var $subclasses = array();
55    var $fields;
56
57    function __construct($model) {
58        $this->model = $model;
59
60        // Merge ModelMeta from parent model (if inherited)
61        $parent = get_parent_class($this->model);
62        $meta = $model::$meta;
63        if ($model::$meta instanceof self)
64            $meta = $meta->meta;
65        if (is_subclass_of($parent, 'VerySimpleModel')) {
66            $this->parent = $parent::getMeta();
67            $meta = $this->parent->extend($this, $meta);
68        }
69        else {
70            $meta = $meta + self::$base;
71        }
72
73        // Short circuit the meta-data processing if APCu is available.
74        // This is preferred as the meta-data is unlikely to change unless
75        // osTicket is upgraded, (then the upgrader calls the
76        // flushModelCache method to clear this cache). Also, GIT_VERSION is
77        // used in the APC key which should be changed if new code is
78        // deployed.
79        if (function_exists('apcu_store')) {
80            $loaded = false;
81            $apc_key = SECRET_SALT.GIT_VERSION."/orm/{$this->model}";
82            $this->meta = apcu_fetch($apc_key, $loaded);
83            if ($loaded)
84                return;
85        }
86
87        if (!$meta['view']) {
88            if (!$meta['table'])
89                throw new OrmConfigurationException(
90                    sprintf(__('%s: Model does not define meta.table'), $this->model));
91            elseif (!$meta['pk'])
92                throw new OrmConfigurationException(
93                    sprintf(__('%s: Model does not define meta.pk'), $this->model));
94        }
95
96        // Ensure other supported fields are set and are arrays
97        foreach (array('pk', 'ordering', 'defer', 'select_related') as $f) {
98            if (!isset($meta[$f]))
99                $meta[$f] = array();
100            elseif (!is_array($meta[$f]))
101                $meta[$f] = array($meta[$f]);
102        }
103
104        // Break down foreign-key metadata
105        foreach ($meta['joins'] as $field => &$j) {
106            $this->processJoin($j);
107            if ($j['local'])
108                $meta['foreign_keys'][$j['local']] = $field;
109        }
110        unset($j);
111        $this->meta = $meta;
112
113        if (function_exists('apcu_store')) {
114            apcu_store($apc_key, $this->meta, 1800);
115        }
116    }
117
118    /**
119     * Merge this class's meta-data into the recieved child meta-data.
120     * When a model extends another model, the meta data for the two models
121     * is merged to form the child's meta data. Returns the merged, child
122     * meta-data.
123     */
124    function extend(ModelMeta $child, $meta) {
125        $this->subclasses[$child->model] = $child;
126        // Merge 'joins' settings (instead of replacing)
127        if (isset($this->meta['joins'])) {
128            $meta['joins'] = array_merge($meta['joins'] ?: array(),
129                $this->meta['joins']);
130        }
131        return $meta + $this->meta + self::$base;
132    }
133
134    function isSuperClassOf($model) {
135        if (isset($this->subclasses[$model]))
136            return true;
137        foreach ($this->subclasses as $M=>$meta)
138            if ($meta->isSuperClassOf($M))
139                return true;
140    }
141
142    function isSubclassOf($model) {
143        if (!isset($this->parent))
144            return false;
145
146        if ($this->parent->model === $model)
147            return true;
148
149        return $this->parent->isSubclassOf($model);
150    }
151
152    /**
153     * Adds some more information to a declared relationship. If the
154     * relationship is a reverse relation, then the information from the
155     * reverse relation is loaded into the local definition
156     *
157     * Compiled-Join-Structure:
158     * 'constraint' => array(local => array(foreign_field, foreign_class)),
159     *      Constraint used to construct a JOIN in an SQL query
160     * 'list' => boolean
161     *      TRUE if an InstrumentedList should be employed to fetch a list
162     *      of related items
163     * 'broker' => Handler for the 'list' property. Usually a subclass of
164     *      'InstrumentedList'
165     * 'null' => boolean
166     *      TRUE if relation is nullable
167     * 'fkey' => array(class, pk)
168     *      Classname and field of the first item in the constraint that
169     *      points to a PK field of a foreign model
170     * 'local' => string
171     *      The local field corresponding to the 'fkey' property
172     */
173    function processJoin(&$j) {
174        $constraint = array();
175        if (isset($j['reverse'])) {
176            list($fmodel, $key) = explode('.', $j['reverse']);
177            // NOTE: It's ok if the forein meta data is not yet inspected.
178            $info = $fmodel::$meta['joins'][$key];
179            if (!is_array($info['constraint']))
180                throw new OrmConfigurationException(sprintf(__(
181                    // `reverse` here is the reverse of an ORM relationship
182                    '%s: Reverse does not specify any constraints'),
183                    $j['reverse']));
184            foreach ($info['constraint'] as $foreign => $local) {
185                list($L,$field) = is_array($local) ? $local : explode('.', $local);
186                $constraint[$field ?: $L] = array($fmodel, $foreign);
187            }
188            if (!isset($j['list']))
189                $j['list'] = true;
190            if (!isset($j['null']))
191                // By default, reverse releationships can be empty lists
192                $j['null'] = true;
193        }
194        else {
195            foreach ($j['constraint'] as $local => $foreign) {
196                list($class, $field) = $constraint[$local]
197                    = is_array($foreign) ? $foreign : explode('.', $foreign);
198            }
199        }
200        if ($j['list'] && !isset($j['broker'])) {
201            $j['broker'] = 'InstrumentedList';
202        }
203        if ($j['broker'] && !class_exists($j['broker'])) {
204            throw new OrmException($j['broker'] . ': List broker does not exist');
205        }
206        foreach ($constraint as $local => $foreign) {
207            list($class, $field) = $foreign;
208            if ($local[0] == "'" || $field[0] == "'" || !class_exists($class))
209                continue;
210            $j['fkey'] = $foreign;
211            $j['local'] = $local;
212        }
213        $j['constraint'] = $constraint;
214    }
215
216    function addJoin($name, array $join) {
217        $this->meta['joins'][$name] = $join;
218        $this->processJoin($this->meta['joins'][$name]);
219    }
220
221    /**
222     * Fetch ModelMeta instance by following a join path from this model
223     */
224    function getByPath($path) {
225        if (is_string($path))
226            $path = explode('__', $path);
227        $root = $this;
228        foreach ($path as $P) {
229            if (!($root = $root['joins'][$P]['fkey'][0]))
230                break;
231            $root = $root::getMeta();
232        }
233        return $root;
234    }
235
236    function offsetGet($field) {
237        return $this->meta[$field];
238    }
239    function offsetSet($field, $what) {
240        $this->meta[$field] = $what;
241    }
242    function offsetExists($field) {
243        return isset($this->meta[$field]);
244    }
245    function offsetUnset($field) {
246        throw new Exception('Model MetaData is immutable');
247    }
248
249    /**
250     * Fetch the column names of the table used to persist instances of this
251     * model in the database.
252     */
253    function getFieldNames() {
254        if (!isset($this->fields))
255            $this->fields = self::inspectFields();
256        return $this->fields;
257    }
258
259    /**
260     * Create a new instance of the model, optionally hydrating it with the
261     * given hash table. The constructor is not called, which leaves the
262     * default constructor free to assume new object status.
263     *
264     * Three methods were considered, with runtime for 10000 iterations
265     *   * unserialze('O:9:"ModelBase":0:{}') - 0.0671s
266     *   * new ReflectionClass("ModelBase")->newInstanceWithoutConstructor()
267     *      - 0.0478s
268     *   * and a hybrid by cloning the reflection class instance - 0.0335s
269     */
270    function newInstance($props=false) {
271        if (!isset($this->new)) {
272            $rc = new ReflectionClass($this->model);
273            $this->new = $rc->newInstanceWithoutConstructor();
274        }
275        $instance = clone $this->new;
276        // Hydrate if props were included
277        if (is_array($props)) {
278            $instance->ht = $props;
279        }
280        return $instance;
281    }
282
283    function inspectFields() {
284        if (!isset(self::$model_cache))
285            self::$model_cache = function_exists('apcu_fetch');
286        if (self::$model_cache) {
287            $key = SECRET_SALT.GIT_VERSION."/orm/{$this['table']}";
288            if ($fields = apcu_fetch($key)) {
289                return $fields;
290            }
291        }
292        $fields = DbEngine::getCompiler()->inspectTable($this['table']);
293        if (self::$model_cache) {
294            apcu_store($key, $fields, 1800);
295        }
296        return $fields;
297    }
298
299    static function flushModelCache() {
300        if (self::$model_cache)
301            @apcu_clear_cache();
302    }
303}
304
305class VerySimpleModel {
306    static $meta = array(
307        'table' => false,
308        'ordering' => false,
309        'pk' => false
310    );
311
312    var $ht = array();
313    var $dirty = array();
314    var $__new__ = false;
315    var $__deleted__ = false;
316    var $__deferred__ = array();
317
318    function __construct($row=false) {
319        if (is_array($row))
320            foreach ($row as $field=>$value)
321                if (!is_array($value))
322                    $this->set($field, $value);
323        $this->__new__ = true;
324    }
325
326    /**
327     * Creates a new instance of the model without calling the constructor.
328     * If the constructor is required, consider using the PHP `new` keyword.
329     * The instance returned from this method will not be considered *new*
330     * and will now result in an INSERT when sent to the database.
331     */
332    static function __hydrate($row=false) {
333        return static::getMeta()->newInstance($row);
334    }
335
336    function __wakeup() {
337        // If a model is stashed in a session, refresh the model from the database
338        $this->refetch();
339    }
340
341    function get($field, $default=false) {
342        if (array_key_exists($field, $this->ht))
343            return $this->ht[$field];
344        elseif (($joins = static::getMeta('joins')) && isset($joins[$field])) {
345            $j = $joins[$field];
346            // Support instrumented lists and such
347            if (isset($j['list']) && $j['list']) {
348                $class = $j['fkey'][0];
349                $fkey = array();
350                // Localize the foreign key constraint
351                foreach ($j['constraint'] as $local=>$foreign) {
352                    list($_klas,$F) = $foreign;
353                    $fkey[$F ?: $_klas] = ($local[0] == "'")
354                        ? trim($local, "'") : $this->ht[$local];
355                }
356                $v = $this->ht[$field] = new $j['broker'](
357                    // Send Model, [Foriegn-Field => Local-Id]
358                    array($class, $fkey)
359                );
360                return $v;
361            }
362            // Support relationships
363            elseif (isset($j['fkey'])) {
364                $criteria = array();
365                foreach ($j['constraint'] as $local => $foreign) {
366                    list($klas,$F) = $foreign;
367                    if (class_exists($klas))
368                        $class = $klas;
369                    if ($local[0] == "'") {
370                        $criteria[$F] = trim($local,"'");
371                    }
372                    elseif ($F[0] == "'") {
373                        // Does not affect the local model
374                        continue;
375                    }
376                    else {
377                        $criteria[$F] = $this->ht[$local];
378                    }
379                }
380                try {
381                    $v = $this->ht[$field] = $class::lookup($criteria);
382                }
383                catch (DoesNotExist $e) {
384                    $v = null;
385                }
386                return $v;
387            }
388        }
389        elseif (isset($this->__deferred__[$field])) {
390            // Fetch deferred field
391            $row = static::objects()->filter($this->getPk())
392                // XXX: Seems like all the deferred fields should be fetched
393                ->values_flat($field)
394                ->one();
395            if ($row)
396                return $this->ht[$field] = $row[0];
397        }
398        elseif ($field == 'pk') {
399            return $this->getPk();
400        }
401
402        if (isset($default))
403            return $default;
404
405        // For new objects, assume the field is NULLable
406        if ($this->__new__)
407            return null;
408
409        // Check to see if the column referenced is actually valid
410        if (in_array($field, static::getMeta()->getFieldNames()))
411            return null;
412
413        throw new OrmException(sprintf(__('%s: %s: Field not defined'),
414            get_class($this), $field));
415    }
416    function __get($field) {
417        return $this->get($field, null);
418    }
419
420    function getByPath($path) {
421        if (is_string($path))
422            $path = explode('__', $path);
423        $root = $this;
424        foreach ($path as $P)
425            $root = $root->get($P);
426        return $root;
427    }
428
429    function __isset($field) {
430        return ($this->ht && array_key_exists($field, $this->ht))
431            || isset(static::$meta['joins'][$field]);
432    }
433
434    function __unset($field) {
435        if ($this->__isset($field))
436            unset($this->ht[$field]);
437        else
438            unset($this->{$field});
439    }
440
441    function set($field, $value) {
442        // Update of foreign-key by assignment to model instance
443        $related = false;
444        $joins = static::getMeta('joins');
445        if (isset($joins[$field])) {
446            $j = $joins[$field];
447            if ($j['list'] && ($value instanceof InstrumentedList)) {
448                // Magic list property
449                $this->ht[$field] = $value;
450                return;
451            }
452            if ($value === null) {
453                $this->ht[$field] = $value;
454                if (in_array($j['local'], static::$meta['pk'])) {
455                    // Reverse relationship — don't null out local PK
456                    return;
457                }
458                // Pass. Set local field to NULL in logic below
459            }
460            elseif ($value instanceof VerySimpleModel) {
461                // Ensure that the model being assigned as a relationship is
462                // an instance of the foreign model given in the
463                // relationship, or is a super class thereof. The super
464                // class case is used primary for the xxxThread classes
465                // which all extend from the base Thread class.
466                if (!$value instanceof $j['fkey'][0]
467                    && !$value::getMeta()->isSuperClassOf($j['fkey'][0])
468                ) {
469                    throw new InvalidArgumentException(
470                        sprintf(__('Expecting NULL or instance of %s. Got a %s instead'),
471                        $j['fkey'][0], is_object($value) ? get_class($value) : gettype($value)));
472                }
473                // Capture the object under the object's field name
474                $this->ht[$field] = $value;
475                $value = $value->get($j['fkey'][1]);
476                // Fall through to the standard logic below
477            }
478            // Capture the foreign key id value
479            $field = $j['local'];
480        }
481        // elseif $field is in a relationship, adjust the relationship
482        elseif (isset(static::$meta['foreign_keys'][$field])) {
483            // meta->foreign_keys->{$field} points to the property of the
484            // foreign object. For instance 'object_id' points to 'object'
485            $related = static::$meta['foreign_keys'][$field];
486        }
487        $old = isset($this->ht[$field]) ? $this->ht[$field] : null;
488        if ($old != $value) {
489            // isset should not be used here, because `null` should not be
490            // replaced in the dirty array
491            if (!array_key_exists($field, $this->dirty))
492                $this->dirty[$field] = $old;
493            if ($related)
494                // $related points to a foreign object propery. If setting a
495                // new object_id value, the relationship to object should be
496                // cleared and rebuilt
497                unset($this->ht[$related]);
498        }
499        $this->ht[$field] = $value;
500    }
501    function __set($field, $value) {
502        return $this->set($field, $value);
503    }
504
505    function setAll($props) {
506        foreach ($props as $field=>$value)
507            $this->set($field, $value);
508    }
509
510    function __onload() {}
511
512    function serialize() {
513        return $this->getPk();
514    }
515
516    function unserialize($data) {
517        $this->ht = $data;
518        $this->refetch();
519    }
520
521    static function getMeta($key=false) {
522        if (!static::$meta instanceof ModelMeta
523            || get_called_class() != static::$meta->model
524        ) {
525            static::$meta = new ModelMeta(get_called_class());
526        }
527        $M = static::$meta;
528        return ($key) ? $M->offsetGet($key) : $M;
529    }
530
531    static function getOrmFields($recurse=false) {
532        $fks = $lfields = $fields = array();
533        $myname = get_called_class();
534        foreach (static::getMeta('joins') as $name=>$j) {
535            $fks[$j['local']] = true;
536            if (!$j['reverse'] && !$j['list'] && $recurse) {
537                foreach ($j['fkey'][0]::getOrmFields($recurse - 1) as $name2=>$f) {
538                    $fields["{$name}__{$name2}"] = "{$name} / $f";
539                }
540            }
541        }
542        foreach (static::getMeta('fields') as $f) {
543            if (isset($fks[$f]))
544                continue;
545            if (in_array($f, static::getMeta('pk')))
546                continue;
547            $lfields[$f] = "{$f}";
548        }
549        return $lfields + $fields;
550    }
551
552    /**
553     * objects
554     *
555     * Retrieve a QuerySet for this model class which can be used to fetch
556     * models from the connected database. Subclasses can override this
557     * method to apply forced constraints on the QuerySet.
558     */
559    static function objects() {
560        return new QuerySet(get_called_class());
561    }
562
563    /**
564     * lookup
565     *
566     * Retrieve a record by its primary key. This method may be short
567     * circuited by model caching if the record has already been loaded by
568     * the database. In such a case, the database will not be consulted for
569     * the model's data.
570     *
571     * This method can be called with an array of keyword arguments matching
572     * the PK of the object or the values of the primary key. Both of these
573     * usages are correct:
574     *
575     * >>> User::lookup(1)
576     * >>> User::lookup(array('id'=>1))
577     *
578     * For composite primary keys and the first usage, pass the values in
579     * the order they are given in the Model's 'pk' declaration in its meta
580     * data.
581     *
582     * Parameters:
583     * $criteria - (mixed) primary key for the sought model either as
584     *      arguments or key/value array as the function's first argument
585     *
586     * Returns:
587     * (Object<Model>|null) a single instance of the sought model or null if
588     * no such instance exists.
589     */
590    static function lookup($criteria) {
591        // Model::lookup(1), where >1< is the pk value
592        $args = func_get_args();
593        if (!is_array($criteria)) {
594            $criteria = array();
595            $pk = static::getMeta('pk');
596            foreach ($args as $i=>$f)
597                $criteria[$pk[$i]] = $f;
598
599            // Only consult cache for PK lookup, which is assumed if the
600            // values are passed as args rather than an array
601            if ($cached = ModelInstanceManager::checkCache(get_called_class(),
602                    $criteria))
603                return $cached;
604        }
605        try {
606            return static::objects()->filter($criteria)->one();
607        }
608        catch (DoesNotExist $e) {
609            return null;
610        }
611    }
612
613    function delete() {
614        $ex = DbEngine::delete($this);
615        try {
616            $ex->execute();
617            if ($ex->affected_rows() != 1)
618                return false;
619
620            $this->__deleted__ = true;
621            Signal::send('model.deleted', $this);
622        }
623        catch (OrmException $e) {
624            return false;
625        }
626        return true;
627    }
628
629    function save($refetch=false) {
630        if ($this->__deleted__)
631            throw new OrmException('Trying to update a deleted object');
632
633        $pk = static::getMeta('pk');
634        $wasnew = $this->__new__;
635
636        // First, if any foreign properties of this object are connected to
637        // another *new* object, then save those objects first and set the
638        // local foreign key field values
639        foreach (static::getMeta('joins') as $prop => $j) {
640            if (isset($this->ht[$prop])
641                && ($foreign = $this->ht[$prop])
642                && $foreign instanceof VerySimpleModel
643                && !in_array($j['local'], $pk)
644                && null === $this->get($j['local'])
645            ) {
646                if ($foreign->__new__ && !$foreign->save())
647                    return false;
648                $this->set($j['local'], $foreign->get($j['fkey'][1]));
649            }
650        }
651
652        // If there's nothing in the model to be saved, then we're done
653        if (count($this->dirty) === 0)
654            return true;
655
656        $ex = DbEngine::save($this);
657        try {
658            $ex->execute();
659            if ($ex->affected_rows() != 1) {
660                // This doesn't really signify an error. It just means that
661                // the database believes that the row did not change. For
662                // inserts though, it's a deal breaker
663                if ($this->__new__)
664                    return false;
665                else
666                    // No need to reload the record if requested — the
667                    // database didn't update anything
668                    $refetch = false;
669            }
670        }
671        catch (OrmException $e) {
672            return false;
673        }
674
675        if ($wasnew) {
676            if (count($pk) == 1)
677                // XXX: Ensure AUTO_INCREMENT is set for the field
678                $this->ht[$pk[0]] = $ex->insert_id();
679            $this->__new__ = false;
680            Signal::send('model.created', $this);
681        }
682        else {
683            $data = array('dirty' => $this->dirty);
684            Signal::send('model.updated', $this, $data);
685            foreach ($this->dirty as $key => $value) {
686                if ($key != 'value' && $key != 'updated') {
687                    $type = array('type' => 'edited', 'key' => $key, 'orm_audit' => true);
688                    Signal::send('object.edited', $this, $type);
689                }
690            }
691        }
692        # Refetch row from database
693        if ($refetch) {
694            // Preserve non database information such as list relationships
695            // across the refetch
696            $this->refetch();
697        }
698        if ($wasnew) {
699            // Attempt to update foreign, unsaved objects with the PK of
700            // this newly created object
701            foreach (static::getMeta('joins') as $prop => $j) {
702                if (isset($this->ht[$prop])
703                    && ($foreign = $this->ht[$prop])
704                    && in_array($j['local'], $pk)
705                ) {
706                    if ($foreign instanceof VerySimpleModel
707                        && null === $foreign->get($j['fkey'][1])
708                    ) {
709                        $foreign->set($j['fkey'][1], $this->get($j['local']));
710                    }
711                    elseif ($foreign instanceof InstrumentedList) {
712                        foreach ($foreign as $item) {
713                            if (null === $item->get($j['fkey'][1]))
714                                $item->set($j['fkey'][1], $this->get($j['local']));
715                        }
716                    }
717                }
718            }
719            $this->__onload();
720        }
721        $this->dirty = array();
722        return true;
723    }
724
725    private function refetch() {
726        try {
727            $this->ht =
728                static::objects()->filter($this->getPk())->values()->one()
729                + $this->ht;
730        } catch (DoesNotExist $ex) {}
731    }
732
733    private function getPk() {
734        $pk = array();
735        foreach ($this::getMeta('pk') as $f)
736            $pk[$f] = $this->ht[$f];
737        return $pk;
738    }
739
740    function getDbFields() {
741        return $this->ht;
742    }
743
744    /**
745     * Create a new clone of this model. The primary key will be unset and the
746     * object will be set as __new__. The __clone() magic method is reserved
747     * by the buildModel() system, because it clone's a single instance when
748     * hydrating objects from the database.
749     */
750    function copy() {
751        // Drop the PK and set as unsaved
752        $dup = clone $this;
753        foreach ($dup::getMeta('pk') as $f)
754            $dup->__unset($f);
755        $dup->__new__ = true;
756        return $dup;
757    }
758}
759
760/**
761 * AnnotatedModel
762 *
763 * Simple wrapper class which allows wrapping and write-protecting of
764 * annotated fields retrieved from the database. Instances of this class
765 * will delegate most all of the heavy lifting to the wrapped Model instance.
766 */
767class AnnotatedModel {
768    static function wrap(VerySimpleModel $model, $extras=array(), $class=false) {
769        static $classes;
770
771        $class = $class ?: get_class($model);
772
773        if ($extras instanceof VerySimpleModel) {
774            $extra = "Writeable";
775        }
776        if (!isset($classes[$class])) {
777            $classes[$class] = eval(<<<END_CLASS
778class {$extra}AnnotatedModel___{$class}
779extends {$class} {
780    protected \$__overlay__;
781    use {$extra}AnnotatedModelTrait;
782
783    static function __hydrate(\$ht=false, \$annotations=false) {
784        \$instance = parent::__hydrate(\$ht);
785        \$instance->__overlay__ = \$annotations;
786        return \$instance;
787    }
788}
789return "{$extra}AnnotatedModel___{$class}";
790END_CLASS
791            );
792        }
793        return $classes[$class]::__hydrate($model->ht, $extras);
794    }
795}
796
797trait AnnotatedModelTrait {
798    function get($what, $default=false) {
799        if (isset($this->__overlay__[$what]))
800            return $this->__overlay__[$what];
801        return parent::get($what);
802    }
803
804    function set($what, $to) {
805        if (isset($this->__overlay__[$what]))
806            throw new OrmException('Annotated fields are read-only');
807        return parent::set($what, $to);
808    }
809
810    function __isset($what) {
811        if (isset($this->__overlay__[$what]))
812            return true;
813        return parent::__isset($what);
814    }
815
816    function getDbFields() {
817        return $this->__overlay__ + parent::getDbFields();
818    }
819}
820
821/**
822 * Slight variant on the AnnotatedModelTrait, except that the overlay is
823 * another model. Its fields are preferred over the wrapped model's fields.
824 * Updates to the overlayed fields are tracked in the overlay model and
825 * therefore kept separate from the annotated model's fields. ::save() will
826 * call save on both models. Delete will only delete the overlay model (that
827 * is, the annotated model will remain).
828 */
829trait WriteableAnnotatedModelTrait {
830    function get($what, $default=false) {
831        if ($this->__overlay__->__isset($what))
832            return $this->__overlay__->get($what);
833        return parent::get($what);
834    }
835
836    function set($what, $to) {
837        if (isset($this->__overlay__)
838            && $this->__overlay__->__isset($what)) {
839            return $this->__overlay__->set($what, $to);
840        }
841        return parent::set($what, $to);
842    }
843
844    function __isset($what) {
845        if (isset($this->__overlay__) && $this->__overlay__->__isset($what))
846            return true;
847        return parent::__isset($what);
848    }
849
850    function getDbFields() {
851        return $this->__overlay__->getDbFields() + parent::getDbFields();
852    }
853
854    function save($refetch=false) {
855        $this->__overlay__->save($refetch);
856        return parent::save($refetch);
857    }
858
859    function delete() {
860        if ($rv = $this->__overlay__->delete())
861            // Mark the annotated object as deleted
862            $this->__deleted__ = true;
863        return $rv;
864    }
865}
866
867class SqlFunction {
868    var $alias;
869
870    function __construct($name) {
871        $this->func = $name;
872        $this->args = array_slice(func_get_args(), 1);
873    }
874
875    function input($what, $compiler, $model) {
876        if ($what instanceof SqlFunction)
877            $A = $what->toSql($compiler, $model);
878        elseif ($what instanceof Q)
879            $A = $compiler->compileQ($what, $model);
880        else
881            $A = $compiler->input($what);
882        return $A;
883    }
884
885    function toSql($compiler, $model=false, $alias=false) {
886        $args = array();
887        foreach ($this->args as $A) {
888            $args[] = $this->input($A, $compiler, $model);
889        }
890        return sprintf('%s(%s)%s', $this->func, implode(', ', $args),
891            $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
892    }
893
894    function getAlias() {
895        return $this->alias;
896    }
897    function setAlias($alias) {
898        $this->alias = $alias;
899    }
900
901    static function __callStatic($func, $args) {
902        $I = new static($func);
903        $I->args = $args;
904        return $I;
905    }
906
907    function __call($operator, $other) {
908        array_unshift($other, $this);
909        return SqlExpression::__callStatic($operator, $other);
910    }
911}
912
913class SqlCase extends SqlFunction {
914    var $cases = array();
915    var $else = false;
916
917    static function N() {
918        return new static('CASE');
919    }
920
921    function when($expr, $result) {
922        if (is_array($expr))
923            $expr = new Q($expr);
924        $this->cases[] = array($expr, $result);
925        return $this;
926    }
927    function otherwise($result) {
928        $this->else = $result;
929        return $this;
930    }
931
932    function toSql($compiler, $model=false, $alias=false) {
933        $cases = array();
934        foreach ($this->cases as $A) {
935            list($expr, $result) = $A;
936            $expr = $this->input($expr, $compiler, $model);
937            $result = $this->input($result, $compiler, $model);
938            $cases[] = "WHEN {$expr} THEN {$result}";
939        }
940        if ($this->else) {
941            $else = $this->input($this->else, $compiler, $model);
942            $cases[] = "ELSE {$else}";
943        }
944        return sprintf('CASE %s END%s', implode(' ', $cases),
945            $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
946    }
947}
948
949class SqlExpr extends SqlFunction {
950    function __construct($args) {
951        $this->args = func_get_args();
952        if (count($this->args) == 1 && is_array($this->args[0]))
953            $this->args = $this->args[0];
954    }
955
956    function toSql($compiler, $model=false, $alias=false) {
957        $O = array();
958        foreach ($this->args as $field=>$value) {
959            if ($value instanceof Q) {
960                $ex = $compiler->compileQ($value, $model, false);
961                $O[] = $ex->text;
962            }
963            else {
964                list($field, $op) = $compiler->getField($field, $model);
965                if (is_callable($op))
966                    $O[] = call_user_func($op, $field, $value, $model);
967                else
968                    $O[] = sprintf($op, $field, $compiler->input($value));
969            }
970        }
971        return implode(' ', $O) . ($alias ? ' AS ' .  $compiler->quote($alias) : '');
972    }
973}
974
975class SqlExpression extends SqlFunction {
976    var $operator;
977    var $operands;
978
979    function toSql($compiler, $model=false, $alias=false) {
980        $O = array();
981        foreach ($this->args as $operand) {
982            $O[] = $this->input($operand, $compiler, $model);
983        }
984        return '('.implode(' '.$this->func.' ', $O)
985            . ($alias ? ' AS '.$compiler->quote($alias) : '')
986            . ')';
987    }
988
989    static function __callStatic($operator, $operands) {
990        switch ($operator) {
991            case 'minus':
992                $operator = '-'; break;
993            case 'plus':
994                $operator = '+'; break;
995            case 'times':
996                $operator = '*'; break;
997            case 'bitand':
998                $operator = '&'; break;
999            case 'bitor':
1000                $operator = '|'; break;
1001            default:
1002                throw new InvalidArgumentException($operator.': Invalid operator specified');
1003        }
1004        return parent::__callStatic($operator, $operands);
1005    }
1006
1007    function __call($operator, $operands) {
1008        array_unshift($operands, $this);
1009        return SqlExpression::__callStatic($operator, $operands);
1010    }
1011}
1012
1013class SqlInterval extends SqlFunction {
1014    var $type;
1015
1016    function toSql($compiler, $model=false, $alias=false) {
1017        $A = $this->args[0];
1018        if ($A instanceof SqlFunction)
1019            $A = $A->toSql($compiler, $model);
1020        else
1021            $A = $compiler->input($A);
1022        return sprintf('INTERVAL %s %s',
1023            $A,
1024            $this->func)
1025            . ($alias ? ' AS '.$compiler->quote($alias) : '');
1026    }
1027
1028    static function __callStatic($interval, $args) {
1029        if (count($args) != 1) {
1030            throw new InvalidArgumentException("Interval expects a single interval value");
1031        }
1032        return parent::__callStatic($interval, $args);
1033    }
1034}
1035
1036class SqlField extends SqlExpression {
1037    var $level;
1038
1039    function __construct($field, $level=0) {
1040        $this->field = $field;
1041        $this->level = $level;
1042    }
1043
1044    function toSql($compiler, $model=false, $alias=false) {
1045        $L = $this->level;
1046        while ($L--)
1047            $compiler = $compiler->getParent();
1048        list($field) = $compiler->getField($this->field, $model);
1049        return $field;
1050    }
1051}
1052
1053class SqlCode extends SqlFunction {
1054    function __construct($code) {
1055        $this->code = $code;
1056    }
1057
1058    function toSql($compiler, $model=false, $alias=false) {
1059        return $this->code.($alias ? ' AS '.$alias : '');
1060    }
1061}
1062
1063class SqlAggregate extends SqlFunction {
1064
1065    var $func;
1066    var $expr;
1067    var $distinct=false;
1068    var $constraint=false;
1069
1070    function __construct($func, $expr, $distinct=false, $constraint=false) {
1071        $this->func = $func;
1072        $this->expr = $expr;
1073        $this->distinct = $distinct;
1074        if ($constraint instanceof Q)
1075            $this->constraint = $constraint;
1076        elseif ($constraint)
1077            $this->constraint = new Q($constraint);
1078    }
1079
1080    static function __callStatic($func, $args) {
1081        $distinct = @$args[1] ?: false;
1082        $constraint = @$args[2] ?: false;
1083        return new static($func, $args[0], $distinct, $constraint);
1084    }
1085
1086    function toSql($compiler, $model=false, $alias=false) {
1087        $options = array('constraint' => $this->constraint, 'model' => true);
1088
1089        // For DISTINCT, require a field specification — not a relationship
1090        // specification.
1091        $E = $this->expr;
1092        if ($E instanceof SqlFunction) {
1093            $field = $E->toSql($compiler, $model);
1094        }
1095        else {
1096        list($field, $rmodel) = $compiler->getField($E, $model, $options);
1097        if ($this->distinct) {
1098            $pk = false;
1099            $fpk  = $rmodel::getMeta('pk');
1100            foreach ($fpk as $f) {
1101                $pk |= false !== strpos($field, $f);
1102            }
1103            if (!$pk) {
1104                // Try and use the foriegn primary key
1105                if (count($fpk) == 1) {
1106                    list($field) = $compiler->getField(
1107                        $this->expr . '__' . $fpk[0],
1108                        $model, $options);
1109                }
1110                else {
1111                    throw new OrmException(
1112                        sprintf('%s :: %s', $rmodel, $field) .
1113                        ': DISTINCT aggregate expressions require specification of a single primary key field of the remote model'
1114                    );
1115                }
1116            }
1117        }
1118        }
1119
1120        return sprintf('%s(%s%s)%s', $this->func,
1121            $this->distinct ? 'DISTINCT ' : '', $field,
1122            $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
1123    }
1124
1125    function getFieldName() {
1126        return strtolower(sprintf('%s__%s', $this->args[0], $this->func));
1127    }
1128}
1129
1130class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countable {
1131    var $model;
1132
1133    var $constraints = array();
1134    var $path_constraints = array();
1135    var $ordering = array();
1136    var $limit = false;
1137    var $offset = 0;
1138    var $related = array();
1139    var $values = array();
1140    var $defer = array();
1141    var $aggregated = false;
1142    var $annotations = array();
1143    var $extra = array();
1144    var $distinct = array();
1145    var $lock = false;
1146    var $chain = array();
1147    var $options = array();
1148
1149    const LOCK_EXCLUSIVE = 1;
1150    const LOCK_SHARED = 2;
1151
1152    const ASC = 'ASC';
1153    const DESC = 'DESC';
1154
1155    const OPT_NOSORT    = 'nosort';
1156    const OPT_NOCACHE   = 'nocache';
1157    const OPT_MYSQL_FOUND_ROWS = 'found_rows';
1158    const OPT_INDEX_HINT = 'indexhint';
1159
1160    const ITER_MODELS   = 1;
1161    const ITER_HASH     = 2;
1162    const ITER_ROW      = 3;
1163
1164    var $iter = self::ITER_MODELS;
1165
1166    var $compiler = 'MySqlCompiler';
1167
1168    var $query;
1169    var $count;
1170    var $total;
1171
1172    function __construct($model) {
1173        $this->model = $model;
1174    }
1175
1176    function filter() {
1177        // Multiple arrays passes means OR
1178        foreach (func_get_args() as $Q) {
1179            $this->constraints[] = $Q instanceof Q ? $Q : new Q($Q);
1180        }
1181        return $this;
1182    }
1183
1184    function exclude() {
1185        foreach (func_get_args() as $Q) {
1186            $this->constraints[] = $Q instanceof Q ? $Q->negate() : Q::not($Q);
1187        }
1188        return $this;
1189    }
1190
1191    /**
1192     * Add a path constraint for the query. This is different from ::filter
1193     * in that the constraint is added to a join clause which is normally
1194     * built from the model meta data. The ::filter() method on the other
1195     * hand adds the constraint to the where clause. This is generally useful
1196     * for aggregate queries and left join queries where multiple rows might
1197     * match a filter in the where clause and would produce incorrect results.
1198     *
1199     * Example:
1200     * Find users with personal email hosted with gmail.
1201     * >>> $Q = User::objects();
1202     * >>> $Q->constrain(['user__emails' => new Q(['type' => 'personal']))
1203     * >>> $Q->filter(['user__emails__address__contains' => '@gmail.com'])
1204     */
1205    function constrain() {
1206        foreach (func_get_args() as $I) {
1207            foreach ($I as $path => $Q) {
1208                if (!is_array($Q) && !$Q instanceof Q) {
1209                    // ->constrain(array('field__path__op' => val));
1210                    $Q = array($path => $Q);
1211                    list(, $path) = SqlCompiler::splitCriteria($path);
1212                    $path = implode('__', $path);
1213                }
1214                $this->path_constraints[$path][] = $Q instanceof Q ? $Q : Q::all($Q);
1215            }
1216        }
1217        return $this;
1218    }
1219
1220    function defer() {
1221        foreach (func_get_args() as $f)
1222            $this->defer[$f] = true;
1223        return $this;
1224    }
1225    function order_by($order, $direction=false) {
1226        if ($order === false)
1227            return $this->options(array('nosort' => true));
1228
1229        $args = func_get_args();
1230        if (in_array($direction, array(self::ASC, self::DESC))) {
1231            $args = array($args[0]);
1232        }
1233        else
1234            $direction = false;
1235
1236        $new = is_array($order) ?  $order : $args;
1237        if ($direction) {
1238            foreach ($new as $i=>$x) {
1239                $new[$i] = array($x, $direction);
1240            }
1241        }
1242        $this->ordering = array_merge($this->ordering, $new);
1243        return $this;
1244    }
1245    function getSortFields() {
1246        $ordering = $this->ordering;
1247        if ($this->extra['order_by'])
1248            $ordering = array_merge($ordering, $this->extra['order_by']);
1249        return $ordering;
1250    }
1251
1252    function lock($how=false) {
1253        $this->lock = $how ?: self::LOCK_EXCLUSIVE;
1254        return $this;
1255    }
1256
1257    function limit($count) {
1258        $this->limit = $count;
1259        return $this;
1260    }
1261
1262    function offset($at) {
1263        $this->offset = $at;
1264        return $this;
1265    }
1266
1267    function isWindowed() {
1268        return $this->limit || $this->offset || (count($this->values) + count($this->annotations) + @count($this->extra['select'])) > 1;
1269    }
1270
1271    /**
1272     * Fetch related fields with the query. This will result in better
1273     * performance as related items are fetched with the root model with
1274     * only one trip to the database.
1275     *
1276     * Either an array of fields can be sent as one argument, or the list of
1277     * fields can be sent as the arguments to the function.
1278     *
1279     * Example:
1280     * >>> $q = User::objects()->select_related('role');
1281     */
1282    function select_related() {
1283        $args = func_get_args();
1284        if (is_array($args[0]))
1285            $args = $args[0];
1286
1287        $this->related = array_merge($this->related, $args);
1288        return $this;
1289    }
1290
1291    function extra(array $extra) {
1292        foreach ($extra as $section=>$info) {
1293            $this->extra[$section] = array_merge($this->extra[$section] ?: array(), $info);
1294        }
1295        return $this;
1296    }
1297
1298    function addExtraJoin(array $join) {
1299       return $this->extra(array('joins' => array($join)));
1300    }
1301
1302    function distinct() {
1303        foreach (func_get_args() as $D)
1304            $this->distinct[] = $D;
1305        return $this;
1306    }
1307
1308    function models() {
1309        $this->iter = self::ITER_MODELS;
1310        $this->values = $this->related = array();
1311        return $this;
1312    }
1313
1314    function values() {
1315        foreach (func_get_args() as $A)
1316            $this->values[$A] = $A;
1317        $this->iter = self::ITER_HASH;
1318        // This disables related models
1319        $this->related = false;
1320        return $this;
1321    }
1322
1323    function values_flat() {
1324        $this->values = func_get_args();
1325        $this->iter = self::ITER_ROW;
1326        // This disables related models
1327        $this->related = false;
1328        return $this;
1329    }
1330
1331    function copy() {
1332        return clone $this;
1333    }
1334
1335    function all() {
1336        return $this->getIterator()->asArray();
1337    }
1338
1339    function first() {
1340        $list = $this->limit(1)->all();
1341        return $list[0];
1342    }
1343
1344    /**
1345     * one
1346     *
1347     * Finds and returns a single model instance based on the criteria in
1348     * this QuerySet instance.
1349     *
1350     * Throws:
1351     * DoesNotExist - if no such model exists with the given criteria
1352     * ObjectNotUnique - if more than one model matches the given criteria
1353     *
1354     * Returns:
1355     * (Object<Model>) a single instance of the sought model is guarenteed.
1356     * If no such model or multiple models exist, an exception is thrown.
1357     */
1358    function one() {
1359        $list = $this->all();
1360        if (count($list) == 0)
1361            throw new DoesNotExist();
1362        elseif (count($list) > 1)
1363            throw new ObjectNotUnique('One object was expected; however '
1364                .'multiple objects in the database matched the query. '
1365                .sprintf('In fact, there are %d matching objects.', count($list))
1366            );
1367        return $list[0];
1368    }
1369
1370    function count() {
1371        // Defer to the iterator if fetching already started
1372        if (isset($this->_iterator)) {
1373            return $this->_iterator->count();
1374        }
1375        elseif (isset($this->count)) {
1376            return $this->count;
1377        }
1378        $class = $this->compiler;
1379        $compiler = new $class();
1380        return $this->count = $compiler->compileCount($this);
1381    }
1382
1383    /**
1384     * Similar to count, except that the LIMIT and OFFSET parts are not
1385     * considered in the counts. That is, this will return the count of rows
1386     * if the query were not windowed with limit() and offset().
1387     *
1388     * For MySQL, the query will be submitted and fetched and the
1389     * SQL_CALC_FOUND_ROWS hint will be sent in the query. Afterwards, the
1390     * result of FOUND_ROWS() is fetched and is the result of this function.
1391     *
1392     * The result of this function is cached. If further changes are made
1393     * after this is run, the changes should be made in a clone.
1394     */
1395    function total() {
1396        if (isset($this->total))
1397            return $this->total;
1398
1399        // Optimize the query with the CALC_FOUND_ROWS if
1400        // - the compiler supports it
1401        // - the iterator hasn't yet been built, that is, the query for this
1402        //   statement has not yet been sent to the database
1403        $compiler = $this->compiler;
1404        if ($compiler::supportsOption(self::OPT_MYSQL_FOUND_ROWS)
1405            && !isset($this->_iterator)
1406        ) {
1407            // This optimization requires caching
1408            $this->options(array(
1409                self::OPT_MYSQL_FOUND_ROWS => 1,
1410                self::OPT_NOCACHE => null,
1411            ));
1412            $this->exists(true);
1413            $compiler = new $compiler();
1414            return $this->total = $compiler->getFoundRows();
1415        }
1416
1417        $query = clone $this;
1418        $query->limit(false)->offset(false)->order_by(false);
1419        return $this->total = $query->count();
1420    }
1421
1422    function toSql($compiler, $model, $alias=false) {
1423        // FIXME: Force root model of the compiler to $model
1424        $exec = $this->getQuery(array('compiler' => get_class($compiler),
1425             'parent' => $compiler, 'subquery' => true));
1426        // Rewrite the parameter numbers so they fit the parameter numbers
1427        // of the current parameters of the $compiler
1428        $sql = preg_replace_callback("/:(\d+)/",
1429        function($m) use ($compiler, $exec) {
1430            $compiler->params[] = $exec->params[$m[1]-1];
1431            return ':'.count($compiler->params);
1432        }, $exec->sql);
1433        return "({$sql})".($alias ? " AS {$alias}" : '');
1434    }
1435
1436    /**
1437     * exists
1438     *
1439     * Determines if there are any rows in this QuerySet. This can be
1440     * achieved either by evaluating a SELECT COUNT(*) query or by
1441     * attempting to fetch the first row from the recordset and return
1442     * boolean success.
1443     *
1444     * Parameters:
1445     * $fetch - (bool) TRUE if a compile and fetch should be attempted
1446     *      instead of a SELECT COUNT(*). This would be recommended if an
1447     *      accurate count is not required and the records would be fetched
1448     *      if this method returns TRUE.
1449     *
1450     * Returns:
1451     * (bool) TRUE if there would be at least one record in this QuerySet
1452     */
1453    function exists($fetch=false) {
1454        if ($fetch) {
1455            return (bool) $this[0];
1456        }
1457        return $this->count() > 0;
1458    }
1459
1460    function annotate($annotations) {
1461        if (!is_array($annotations))
1462            $annotations = func_get_args();
1463        foreach ($annotations as $name=>$A) {
1464            if ($A instanceof SqlFunction) {
1465                if (is_int($name))
1466                    $name = $A->getFieldName();
1467                $A->setAlias($name);
1468            }
1469            $this->annotations[$name] = $A;
1470        }
1471        return $this;
1472    }
1473
1474    function aggregate($annotations) {
1475        // Aggregate works like annotate, except that it sets up values
1476        // fetching which will disable model creation
1477        $this->annotate($annotations);
1478        $this->values();
1479        // Disable other fields from being fetched
1480        $this->aggregated = true;
1481        $this->related = false;
1482        return $this;
1483    }
1484
1485    function options($options) {
1486        // Make an array with $options as the only key
1487        if (!is_array($options))
1488            $options = array($options => 1);
1489
1490        $this->options = array_merge($this->options, $options);
1491        return $this;
1492    }
1493
1494    function hasOption($option) {
1495        return isset($this->options[$option]);
1496    }
1497
1498    function getOption($option) {
1499        return @$this->options[$option] ?: false;
1500    }
1501
1502    function setOption($option, $value) {
1503        $this->options[$option] = $value;
1504    }
1505
1506    function clearOption($option) {
1507        unset($this->options[$option]);
1508    }
1509
1510    function countSelectFields() {
1511        $count = count($this->values) + count($this->annotations);
1512        if (isset($this->extra['select']))
1513            foreach (@$this->extra['select'] as $S)
1514                $count += count($S);
1515        return $count;
1516    }
1517
1518    function union(QuerySet $other, $all=true) {
1519        // Values and values_list _must_ match for this to work
1520        if ($this->countSelectFields() != $other->countSelectFields())
1521            throw new OrmException('Union queries must have matching values counts');
1522
1523        // TODO: Clear OFFSET and LIMIT in the $other query
1524
1525        $this->chain[] = array($other, $all);
1526        return $this;
1527    }
1528
1529    function delete() {
1530        $class = $this->compiler;
1531        $compiler = new $class();
1532        // XXX: Mark all in-memory cached objects as deleted
1533        $ex = $compiler->compileBulkDelete($this);
1534        $ex->execute();
1535        return $ex->affected_rows();
1536    }
1537
1538    function update(array $what) {
1539        $class = $this->compiler;
1540        $compiler = new $class;
1541        $ex = $compiler->compileBulkUpdate($this, $what);
1542        $ex->execute();
1543        return $ex->affected_rows();
1544    }
1545
1546    function __clone() {
1547        unset($this->_iterator);
1548        unset($this->query);
1549        unset($this->count);
1550        unset($this->total);
1551    }
1552
1553    function __call($name, $args) {
1554
1555        if (!is_callable(array($this->getIterator(), $name)))
1556            throw new OrmException('Call to undefined method QuerySet::'.$name);
1557
1558        return $args
1559            ? call_user_func_array(array($this->getIterator(), $name), $args)
1560            : call_user_func(array($this->getIterator(), $name));
1561    }
1562
1563    // IteratorAggregate interface
1564    function getIterator($iterator=false) {
1565        if (!isset($this->_iterator)) {
1566            $class = $iterator ?: $this->getIteratorClass();
1567            $it = new $class($this);
1568            if (!$this->hasOption(self::OPT_NOCACHE)) {
1569                if ($this->iter == self::ITER_MODELS)
1570                    // Add findFirst() and such
1571                    $it = new ModelResultSet($it);
1572                else
1573                    $it = new CachedResultSet($it);
1574            }
1575            else {
1576                $it = $it->getIterator();
1577            }
1578            $this->_iterator = $it;
1579        }
1580        return $this->_iterator;
1581    }
1582
1583    function getIteratorClass() {
1584        switch ($this->iter) {
1585        case self::ITER_MODELS:
1586            return 'ModelInstanceManager';
1587        case self::ITER_HASH:
1588            return 'HashArrayIterator';
1589        case self::ITER_ROW:
1590            return 'FlatArrayIterator';
1591        }
1592    }
1593
1594    // ArrayAccess interface
1595    function offsetExists($offset) {
1596        return $this->getIterator()->offsetExists($offset);
1597    }
1598    function offsetGet($offset) {
1599        return $this->getIterator()->offsetGet($offset);
1600    }
1601    function offsetUnset($a) {
1602        throw new Exception(__('QuerySet is read-only'));
1603    }
1604    function offsetSet($a, $b) {
1605        throw new Exception(__('QuerySet is read-only'));
1606    }
1607
1608    function __toString() {
1609        return (string) $this->getQuery();
1610    }
1611
1612    function getQuery($options=array()) {
1613        if (isset($this->query))
1614            return $this->query;
1615
1616        // Load defaults from model
1617        $model = $this->model;
1618        $meta = $model::getMeta();
1619        $query = clone $this;
1620        $options += $this->options;
1621        if ($options['nosort'])
1622            $query->ordering = array();
1623        elseif (!$query->ordering && $meta['ordering'])
1624            $query->ordering = $meta['ordering'];
1625        if (false !== $query->related && !$query->related && !$query->values && $meta['select_related'])
1626            $query->related = $meta['select_related'];
1627        if (!$query->defer && $meta['defer'])
1628            $query->defer = $meta['defer'];
1629
1630        $class = $options['compiler'] ?: $this->compiler;
1631        $compiler = new $class($options);
1632        $this->query = $compiler->compileSelect($query);
1633
1634        return $this->query;
1635    }
1636
1637    /**
1638     * Fetch a model class which can be used to render the QuerySet as a
1639     * subquery to be used as a JOIN.
1640     */
1641    function asView() {
1642        $unique = spl_object_hash($this);
1643        $classname = "QueryView{$unique}";
1644
1645        if (class_exists($classname))
1646            return $classname;
1647
1648        $class = <<<EOF
1649class {$classname} extends VerySimpleModel {
1650    static \$meta = array(
1651        'view' => true,
1652    );
1653    static \$queryset;
1654
1655    static function getQuery(\$compiler) {
1656        return ' ('.static::\$queryset->getQuery().') ';
1657    }
1658
1659    static function getSqlAddParams(\$compiler) {
1660        return static::\$queryset->toSql(\$compiler, self::\$queryset->model);
1661    }
1662}
1663EOF;
1664        eval($class); // Ugh
1665        $classname::$queryset = $this;
1666        return $classname;
1667    }
1668
1669    function serialize() {
1670        $info = get_object_vars($this);
1671        unset($info['query']);
1672        unset($info['limit']);
1673        unset($info['offset']);
1674        unset($info['_iterator']);
1675        unset($info['count']);
1676        unset($info['total']);
1677        return serialize($info);
1678    }
1679
1680    function unserialize($data) {
1681        $data = unserialize($data);
1682        foreach ($data as $name => $val) {
1683            $this->{$name} = $val;
1684        }
1685    }
1686}
1687
1688class DoesNotExist extends Exception {}
1689class ObjectNotUnique extends Exception {}
1690
1691class CachedResultSet
1692extends BaseList
1693implements ArrayAccess {
1694    protected $inner;
1695    protected $eoi = false;
1696
1697    function __construct(IteratorAggregate $iterator) {
1698        $this->inner = $iterator->getIterator();
1699    }
1700
1701    function fillTo($level) {
1702        while (!$this->eoi && count($this->storage) < $level) {
1703            if (!$this->inner->valid()) {
1704                $this->eoi = true;
1705                break;
1706            }
1707            $this->storage[] = $this->inner->current();
1708            $this->inner->next();
1709        }
1710    }
1711
1712    function asArray() {
1713        $this->fillTo(PHP_INT_MAX);
1714        return $this->getCache();
1715    }
1716
1717    function getCache() {
1718        return $this->storage;
1719    }
1720
1721    function reset() {
1722        $this->eoi = false;
1723        $this->storage = array();
1724        // XXX: Should the inner be recreated to refetch?
1725        $this->inner->rewind();
1726    }
1727
1728    function getIterator() {
1729        $this->asArray();
1730        return new ArrayIterator($this->storage);
1731    }
1732
1733    function offsetExists($offset) {
1734        $this->fillTo($offset+1);
1735        return count($this->storage) > $offset;
1736    }
1737    function offsetGet($offset) {
1738        $this->fillTo($offset+1);
1739        return $this->storage[$offset];
1740    }
1741    function offsetUnset($a) {
1742        throw new Exception(__('QuerySet is read-only'));
1743    }
1744    function offsetSet($a, $b) {
1745        throw new Exception(__('QuerySet is read-only'));
1746    }
1747
1748    function count($mode=COUNT_NORMAL) {
1749        $this->asArray();
1750        return count($this->storage);
1751    }
1752
1753    /**
1754     * Sort the instrumented list in place. This would be useful to change the
1755     * sorting order of the items in the list without fetching the list from
1756     * the database again.
1757     *
1758     * Parameters:
1759     * $key - (callable|int) A callable function to produce the sort keys
1760     *      or one of the SORT_ constants used by the array_multisort
1761     *      function
1762     * $reverse - (bool) true if the list should be sorted descending
1763     *
1764     * Returns:
1765     * This instrumented list for chaining and inlining.
1766     */
1767    function sort($key=false, $reverse=false) {
1768        // Fetch all records into the cache
1769        $this->asArray();
1770        parent::sort($key, $reverse);
1771        return $this;
1772    }
1773
1774    /**
1775     * Reverse the list item in place. Returns this object for chaining
1776     */
1777    function reverse() {
1778        $this->asArray();
1779        return parent::reverse();
1780    }
1781}
1782
1783class ModelResultSet
1784extends CachedResultSet {
1785    /**
1786     * Find the first item in the current set which matches the given criteria.
1787     * This would be used in favor of ::filter() which might trigger another
1788     * database query. The criteria is intended to be quite simple and should
1789     * not traverse relationships which have not already been fetched.
1790     * Otherwise, the ::filter() or ::window() methods would provide better
1791     * performance.
1792     *
1793     * Example:
1794     * >>> $a = new User();
1795     * >>> $a->roles->add(Role::lookup(['name' => 'administator']));
1796     * >>> $a->roles->findFirst(['roles__name__startswith' => 'admin']);
1797     * <Role: administrator>
1798     */
1799    function findFirst($criteria) {
1800        $records = $this->findAll($criteria, 1);
1801        return count($records) > 0 ? $records[0] : null;
1802    }
1803
1804    /**
1805     * Find all the items in the current set which match the given criteria.
1806     * This would be used in favor of ::filter() which might trigger another
1807     * database query. The criteria is intended to be quite simple and should
1808     * not traverse relationships which have not already been fetched.
1809     * Otherwise, the ::filter() or ::window() methods would provide better
1810     * performance, as they can provide results with one more trip to the
1811     * database.
1812     */
1813    function findAll($criteria, $limit=false) {
1814        $records = new ListObject();
1815        foreach ($this as $record) {
1816            $matches = true;
1817            foreach ($criteria as $field=>$check) {
1818                if (!SqlCompiler::evaluate($record, $check, $field)) {
1819                    $matches = false;
1820                    break;
1821                }
1822            }
1823            if ($matches)
1824                $records[] = $record;
1825            if ($limit && count($records) == $limit)
1826                break;
1827        }
1828        return $records;
1829    }
1830}
1831
1832class ModelInstanceManager
1833implements IteratorAggregate {
1834    var $model;
1835    var $map;
1836    var $resource;
1837    var $annnotations;
1838    var $defer;
1839
1840    static $objectCache = array();
1841
1842    function __construct(QuerySet $queryset) {
1843        $this->model = $queryset->model;
1844        $this->resource = $queryset->getQuery();
1845        $cache = !$queryset->hasOption(QuerySet::OPT_NOCACHE);
1846        $this->resource->setBuffered($cache);
1847        $this->map = $this->resource->getMap();
1848        $this->annotations = $queryset->annotations;
1849        $this->defer = $queryset->defer;
1850    }
1851
1852    function cache($model) {
1853        $key = sprintf('%s.%s',
1854            $model::$meta->model, implode('.', $model->get('pk')));
1855        self::$objectCache[$key] = $model;
1856    }
1857
1858    /**
1859     * uncache
1860     *
1861     * Drop the cached reference to the model. If the model is deleted
1862     * database-side. Lookups for the same model should not be short
1863     * circuited to retrieve the cached reference.
1864     */
1865    static function uncache($model) {
1866        $key = sprintf('%s.%s',
1867            $model::$meta->model, implode('.', $model->pk));
1868        unset(self::$objectCache[$key]);
1869    }
1870
1871    static function flushCache() {
1872        self::$objectCache = array();
1873    }
1874
1875    static function checkCache($modelClass, $fields) {
1876        $key = $modelClass::$meta->model;
1877        foreach ($modelClass::getMeta('pk') as $f)
1878            $key .= '.'.$fields[$f];
1879        return @self::$objectCache[$key];
1880    }
1881
1882    /**
1883     * getOrBuild
1884     *
1885     * Builds a new model from the received fields or returns the model
1886     * already stashed in the model cache. Caching helps to ensure that
1887     * multiple lookups for the same model identified by primary key will
1888     * fetch the exact same model. Therefore, changes made to the model
1889     * anywhere in the project will be reflected everywhere.
1890     *
1891     * For annotated models (models build from querysets with annotations),
1892     * the built or cached model is wrapped in an AnnotatedModel instance.
1893     * The annotated fields are in the AnnotatedModel instance and the
1894     * database-backed fields are managed by the Model instance.
1895     */
1896    function getOrBuild($modelClass, $fields, $cache=true) {
1897        // Check for NULL primary key, used with related model fetching. If
1898        // the PK is NULL, then consider the object to also be NULL
1899        foreach ($modelClass::getMeta('pk') as $pkf) {
1900            if (!isset($fields[$pkf])) {
1901                return null;
1902            }
1903        }
1904        $annotations = $this->annotations;
1905        $extras = array();
1906        // For annotations, drop them from the $fields list and add them to
1907        // an $extras list. The fields passed to the root model should only
1908        // be the root model's fields. The annotated fields will be wrapped
1909        // using an AnnotatedModel instance.
1910        if ($annotations && $modelClass == $this->model) {
1911            foreach ($annotations as $name=>$A) {
1912                if (array_key_exists($name, $fields)) {
1913                    $extras[$name] = $fields[$name];
1914                    unset($fields[$name]);
1915                }
1916            }
1917        }
1918        // Check the cache for the model instance first
1919        if (!($m = self::checkCache($modelClass, $fields))) {
1920            // Construct and cache the object
1921            $m = $modelClass::__hydrate($fields);
1922            // XXX: defer may refer to fields not in this model
1923            $m->__deferred__ = $this->defer;
1924            $m->__onload();
1925            if ($cache)
1926                $this->cache($m);
1927        }
1928        // Wrap annotations in an AnnotatedModel
1929        if ($extras) {
1930            $m = AnnotatedModel::wrap($m, $extras, $modelClass);
1931        }
1932        // TODO: If the model has deferred fields which are in $fields,
1933        // those can be resolved here
1934        return $m;
1935    }
1936
1937    /**
1938     * buildModel
1939     *
1940     * This method builds the model including related models from the record
1941     * received. For related recordsets, a $map should be setup inside this
1942     * object prior to using this method. The $map is assumed to have this
1943     * configuration:
1944     *
1945     * array(array(<fieldNames>, <modelClass>, <relativePath>))
1946     *
1947     * Where $modelClass is the name of the foreign (with respect to the
1948     * root model ($this->model), $fieldNames is the number and names of
1949     * fields in the row for this model, $relativePath is the path that
1950     * describes the relationship between the root model and this model,
1951     * 'user__account' for instance.
1952     */
1953    function buildModel($row, $cache=true) {
1954        // TODO: Traverse to foreign keys
1955        if ($this->map) {
1956            if ($this->model != $this->map[0][1])
1957                throw new OrmException('Internal select_related error');
1958
1959            $offset = 0;
1960            foreach ($this->map as $info) {
1961                @list($fields, $model_class, $path) = $info;
1962                $values = array_slice($row, $offset, count($fields));
1963                $record = array_combine($fields, $values);
1964                if (!$path) {
1965                    // Build the root model
1966                    $model = $this->getOrBuild($this->model, $record, $cache);
1967                }
1968                elseif ($model) {
1969                    $i = 0;
1970                    // Traverse the declared path and link the related model
1971                    $tail = array_pop($path);
1972                    $m = $model;
1973                    foreach ($path as $field) {
1974                        if (!($m = $m->get($field)))
1975                            break;
1976                    }
1977                    if ($m) {
1978                        // Only apply cache setting to the root model.
1979                        // Reference models should use caching
1980                        $m->set($tail, $this->getOrBuild($model_class, $record, $cache));
1981                    }
1982                }
1983                $offset += count($fields);
1984            }
1985        }
1986        else {
1987            $model = $this->getOrBuild($this->model, $row, $cache);
1988        }
1989        return $model;
1990    }
1991
1992    function getIterator() {
1993        $func = ($this->map) ? 'getRow' : 'getArray';
1994        $func = array($this->resource, $func);
1995
1996        return new CallbackSimpleIterator(function() use ($func, $cache) {
1997            global $StopIteration;
1998
1999            if ($row = $func())
2000                return $this->buildModel($row, $cache);
2001
2002            $this->resource->close();
2003            throw $StopIteration;
2004        });
2005    }
2006}
2007
2008class CallbackSimpleIterator
2009implements Iterator {
2010    var $current;
2011    var $eoi;
2012    var $callback;
2013    var $key = -1;
2014
2015    function __construct($callback) {
2016        assert(is_callable($callback));
2017        $this->callback = $callback;
2018    }
2019
2020    function rewind() {
2021        $this->eoi = false;
2022        $this->next();
2023    }
2024
2025    function key() {
2026        return $this->key;
2027    }
2028
2029    function valid() {
2030        if (!isset($this->eoi))
2031            $this->rewind();
2032        return !$this->eoi;
2033    }
2034
2035    function current() {
2036        if ($this->eoi) return false;
2037        return $this->current;
2038    }
2039
2040    function next() {
2041        try {
2042            $cbk = $this->callback;
2043            $this->current = $cbk();
2044            $this->key++;
2045        }
2046        catch (StopIteration $x) {
2047            $this->eoi = true;
2048        }
2049    }
2050}
2051
2052// Use a global variable, as constructing exceptions is expensive
2053class StopIteration extends Exception {}
2054$StopIteration = new StopIteration();
2055
2056class FlatArrayIterator
2057implements IteratorAggregate {
2058    var $queryset;
2059    var $resource;
2060
2061    function __construct(QuerySet $queryset) {
2062        $this->queryset = $queryset;
2063    }
2064
2065    function getIterator() {
2066        $this->resource = $this->queryset->getQuery();
2067        return new CallbackSimpleIterator(function() {
2068            global $StopIteration;
2069
2070            if ($row = $this->resource->getRow())
2071                return $row;
2072
2073            $this->resource->close();
2074            throw $StopIteration;
2075        });
2076    }
2077}
2078
2079class HashArrayIterator
2080implements IteratorAggregate {
2081    var $queryset;
2082    var $resource;
2083
2084    function __construct(QuerySet $queryset) {
2085        $this->queryset = $queryset;
2086    }
2087
2088    function getIterator() {
2089        $this->resource = $this->queryset->getQuery();
2090        return new CallbackSimpleIterator(function() {
2091            global $StopIteration;
2092
2093            if ($row = $this->resource->getArray())
2094                return $row;
2095
2096            $this->resource->close();
2097            throw $StopIteration;
2098        });
2099    }
2100}
2101
2102class InstrumentedList
2103extends ModelResultSet {
2104    var $key;
2105
2106    function __construct($fkey, $queryset=false,
2107        $iterator='ModelInstanceManager'
2108    ) {
2109        list($model, $this->key) = $fkey;
2110        if (!$queryset) {
2111            $queryset = $model::objects()->filter($this->key);
2112            if ($related = $model::getMeta('select_related'))
2113                $queryset->select_related($related);
2114        }
2115        parent::__construct(new $iterator($queryset));
2116        $this->model = $model;
2117        $this->queryset = $queryset;
2118    }
2119
2120    function add($object, $save=true, $at=false) {
2121        // NOTE: Attempting to compare $object to $this->model will likely
2122        // be problematic, and limits creative applications of the ORM
2123        if (!$object) {
2124            throw new Exception(sprintf(
2125                'Attempting to add invalid object to list. Expected <%s>, but got <NULL>',
2126                $this->model
2127            ));
2128        }
2129
2130        foreach ($this->key as $field=>$value)
2131            $object->set($field, $value);
2132
2133        if (!$object->__new__ && $save)
2134            $object->save();
2135
2136        if ($at !== false)
2137            $this->storage[$at] = $object;
2138        else
2139            $this->storage[] = $object;
2140
2141        return $object;
2142    }
2143
2144    function merge(InstrumentedList $list, $save=false) {
2145       foreach ($list as $object)
2146         $this->add($object, $save);
2147
2148       return $this;
2149    }
2150
2151    function remove($object, $delete=true) {
2152        if ($delete)
2153            $object->delete();
2154        else
2155            foreach ($this->key as $field=>$value)
2156                $object->set($field, null);
2157    }
2158
2159    /**
2160     * Slight edit to the standard iteration method which will skip deleted
2161     * items.
2162     */
2163    function getIterator() {
2164        return new CallbackFilterIterator(parent::getIterator(),
2165            function($i) { return !$i->__deleted__; }
2166        );
2167    }
2168
2169    /**
2170     * Reduce the list to a subset using a simply key/value constraint. New
2171     * items added to the subset will have the constraint automatically
2172     * added to all new items.
2173     *
2174     * Parameters:
2175     * $criteria - (<Traversable>) criteria by which this list will be
2176     *    constrained and filtered.
2177     * $evaluate - (<bool>) if set to TRUE, the criteria will be evaluated
2178     *    without making any more trips to the database. NOTE this may yield
2179     *    unexpected results if this list does not contain all the records
2180     *    from the database which would be matched by another query.
2181     */
2182    function window($constraint, $evaluate=false) {
2183        $model = $this->model;
2184        $fields = $model::getMeta()->getFieldNames();
2185        $key = $this->key;
2186        foreach ($constraint as $field=>$value) {
2187            if (!is_string($field) || false === in_array($field, $fields))
2188                throw new OrmException('InstrumentedList windowing must be performed on local fields only');
2189            $key[$field] = $value;
2190        }
2191        $list = new static(array($this->model, $key), $this->filter($constraint));
2192        if ($evaluate) {
2193            $list->setCache($this->findAll($constraint));
2194        }
2195        return $list;
2196    }
2197
2198    /**
2199     * Disable database fetching on this list by providing a static list of
2200     * objects. ::add() and ::remove() are still supported.
2201     * XXX: Move this to a parent class?
2202     */
2203    function setCache(array $cache) {
2204        if (count($this->storage) > 0)
2205            throw new Exception('Cache must be set before fetching records');
2206        // Set cache and disable fetching
2207        $this->reset();
2208        $this->storage = $cache;
2209    }
2210
2211    // Save all changes made to any list items
2212    function saveAll() {
2213        foreach ($this as $I)
2214            if (!$I->save())
2215                return false;
2216        return true;
2217    }
2218
2219    // QuerySet delegates
2220    function exists() {
2221        return $this->queryset->exists();
2222    }
2223    function expunge() {
2224        if ($this->queryset->delete())
2225            $this->reset();
2226    }
2227    function update(array $what) {
2228        return $this->queryset->update($what);
2229    }
2230
2231    // Fetch a new QuerySet
2232    function objects() {
2233        return clone $this->queryset;
2234    }
2235
2236    function offsetUnset($a) {
2237        $this->fillTo($a);
2238        $this->storage[$a]->delete();
2239    }
2240    function offsetSet($a, $b) {
2241        $this->fillTo($a);
2242        if ($obj = $this->storage[$a])
2243            $obj->delete();
2244        $this->add($b, true, $a);
2245    }
2246
2247    // QuerySet overriedes
2248    function __call($what, $how) {
2249        return call_user_func_array(array($this->objects(), $what), $how);
2250    }
2251}
2252
2253class SqlCompiler {
2254    var $options = array();
2255    var $params = array();
2256    var $joins = array();
2257    var $aliases = array();
2258    var $alias_num = 1;
2259
2260    static $operators = array(
2261        'exact' => '%$1s = %$2s'
2262    );
2263
2264    function __construct($options=false) {
2265        if (is_array($options)) {
2266            $this->options = array_merge($this->options, $options);
2267            if (isset($options['subquery']))
2268                $this->alias_num += 150;
2269        }
2270    }
2271
2272    function getParent() {
2273        return $this->options['parent'];
2274    }
2275
2276    /**
2277     * Split a criteria item into the identifying pieces: path, field, and
2278     * operator.
2279     */
2280    static function splitCriteria($criteria) {
2281        static $operators = array(
2282            'exact' => 1, 'isnull' => 1,
2283            'gt' => 1, 'lt' => 1, 'gte' => 1, 'lte' => 1, 'range' => 1,
2284            'contains' => 1, 'like' => 1, 'startswith' => 1, 'endswith' => 1, 'regex' => 1,
2285            'in' => 1, 'intersect' => 1,
2286            'hasbit' => 1,
2287        );
2288        $path = explode('__', $criteria);
2289        if (!isset($options['table'])) {
2290            $field = array_pop($path);
2291            if (isset($operators[$field])) {
2292                $operator = $field;
2293                $field = array_pop($path);
2294            }
2295        }
2296        return array($field, $path, $operator ?: 'exact');
2297    }
2298
2299    /**
2300     * Check if the values match given the operator.
2301     *
2302     * Parameters:
2303     * $record - <ModelBase> An model instance representing a row from the
2304     *      database
2305     * $field - Field path including operator used as the evaluated
2306     *      expression base. To check if field `name` startswith something,
2307     *      $field would be `name__startswith`.
2308     * $check - <mixed> value used as the comparison. This would be the RHS
2309     *      of the condition expressed with $field. This can also be a Q
2310     *      instance, in which case, $field is not considered, and the Q
2311     *      will be used to evaluate the $record directly.
2312     *
2313     * Throws:
2314     * OrmException - if $operator is not supported
2315     */
2316    static function evaluate($record, $check, $field) {
2317        static $ops; if (!isset($ops)) { $ops = array(
2318            'exact' => function($a, $b) { return is_string($a) ? strcasecmp($a, $b) == 0 : $a == $b; },
2319            'isnull' => function($a, $b) { return is_null($a) == $b; },
2320            'gt' => function($a, $b) { return $a > $b; },
2321            'gte' => function($a, $b) { return $a >= $b; },
2322            'lt' => function($a, $b) { return $a < $b; },
2323            'lte' => function($a, $b) { return $a <= $b; },
2324            'in' => function($a, $b) { return in_array($a, is_array($b) ? $b : array($b)); },
2325            'contains' => function($a, $b) { return stripos($a, $b) !== false; },
2326            'startswith' => function($a, $b) { return stripos($a, $b) === 0; },
2327            'endswith' => function($a, $b) { return iEndsWith($a, $b); },
2328            'regex' => function($a, $b) { return preg_match("/$a/iu", $b); },
2329            'hasbit' => function($a, $b) { return ($a & $b) == $b; },
2330        ); }
2331        // TODO: Support Q expressions
2332        if ($check instanceof Q)
2333            return $check->evaluate($record);
2334
2335        list($field, $path, $operator) = self::splitCriteria($field);
2336        if (!isset($ops[$operator]))
2337            throw new OrmException($operator.': Unsupported operator');
2338
2339        if ($path)
2340            $record = $record->getByPath($path);
2341
2342        return $ops[$operator]($record->get($field), $check);
2343    }
2344
2345    /**
2346     * Handles breaking down a field or model search descriptor into the
2347     * model search path, field, and operator parts. When used in a queryset
2348     * filter, an expression such as
2349     *
2350     * user__email__hostname__contains => 'foobar'
2351     *
2352     * would be broken down to search from the root model (passed in,
2353     * perhaps a ticket) to the user and email models by inspecting the
2354     * model metadata 'joins' property. The 'constraint' value found there
2355     * will be used to build the JOIN sql clauses.
2356     *
2357     * The 'hostname' will be the field on 'email' model that should be
2358     * compared in the WHERE clause. The comparison should be made using a
2359     * 'contains' function, which in MySQL, might be implemented using
2360     * something like "<lhs> LIKE '%foobar%'"
2361     *
2362     * This function will rely heavily on the pushJoin() function which will
2363     * handle keeping track of joins made previously in the query and
2364     * therefore prevent multiple joins to the same table for the same
2365     * reason. (Self joins are still supported).
2366     *
2367     * Comparison functions supported by this function are defined for each
2368     * respective SqlCompiler subclass; however at least these functions
2369     * should be defined:
2370     *
2371     *      function    a__function => b
2372     *      ----------+------------------------------------------------
2373     *      exact     | a is exactly equal to b
2374     *      gt        | a is greater than b
2375     *      lte       | b is greater than a
2376     *      lt        | a is less than b
2377     *      gte       | b is less than a
2378     *      ----------+------------------------------------------------
2379     *      contains  | (string) b is contained within a
2380     *      statswith | (string) first len(b) chars of a are exactly b
2381     *      endswith  | (string) last len(b) chars of a are exactly b
2382     *      like      | (string) a matches pattern b
2383     *      ----------+------------------------------------------------
2384     *      in        | a is in the list or the nested queryset b
2385     *      ----------+------------------------------------------------
2386     *      isnull    | a is null (if b) else a is not null
2387     *
2388     * If no comparison function is declared in the field descriptor,
2389     * 'exact' is assumed.
2390     *
2391     * Parameters:
2392     * $field - (string) name of the field to join
2393     * $model - (VerySimpleModel) root model for references in the $field
2394     *      parameter
2395     * $options - (array) extra options for the compiler
2396     *      'table' => return the table alias rather than the field-name
2397     *      'model' => return the target model class rather than the operator
2398     *      'constraint' => extra constraint for join clause
2399     *
2400     * Returns:
2401     * (mixed) Usually array<field-name, operator> where field-name is the
2402     * name of the field in the destination model, and operator is the
2403     * requestion comparison method.
2404     */
2405    function getField($field, $model, $options=array()) {
2406        // Break apart the field descriptor by __ (double-underbars). The
2407        // first part is assumed to be the root field in the given model.
2408        // The parts after each of the __ pieces are links to other tables.
2409        // The last item (after the last __) is allowed to be an operator
2410        // specifiction.
2411        list($field, $parts, $op) = static::splitCriteria($field);
2412        $operator = static::$operators[$op];
2413        $path = '';
2414        $rootModel = $model;
2415
2416        // Call pushJoin for each segment in the join path. A new JOIN
2417        // fragment will need to be emitted and/or cached
2418        $joins = array();
2419        $null = false;
2420        $push = function($p, $model) use (&$joins, &$path, &$null) {
2421            $J = $model::getMeta('joins');
2422            if (!($info = $J[$p])) {
2423                throw new OrmException(sprintf(
2424                   'Model `%s` does not have a relation called `%s`',
2425                    $model, $p));
2426            }
2427            // Propogate LEFT joins through other joins. That is, if a
2428            // multi-join expression is used, the first LEFT join should
2429            // result in further joins also being LEFT
2430            if (isset($info['null']))
2431                $null = $null || $info['null'];
2432            $info['null'] = $null;
2433            $crumb = $path;
2434            $path = ($path) ? "{$path}__{$p}" : $p;
2435            $joins[] = array($crumb, $path, $model, $info);
2436            // Roll to foreign model
2437            return $info['fkey'];
2438        };
2439
2440        foreach ($parts as $p) {
2441            list($model) = $push($p, $model);
2442        }
2443
2444        // If comparing a relationship, join the foreign table
2445        // This is a comparison with a relationship — use the foreign key
2446        $J = $model::getMeta('joins');
2447        if (isset($J[$field])) {
2448            list($model, $field) = $push($field, $model);
2449        }
2450
2451        // Apply the joins list to $this->pushJoin
2452        $last = count($joins) - 1;
2453        $constraint = false;
2454        foreach ($joins as $i=>$A) {
2455            // Add the conststraint as the last arg to the last join
2456            if ($i == $last)
2457                $constraint = $options['constraint'];
2458            $alias = $this->pushJoin($A[0], $A[1], $A[2], $A[3], $constraint);
2459        }
2460
2461        if (!isset($alias)) {
2462            // Determine the alias for the root model table
2463            $alias = (isset($this->joins['']))
2464                ? $this->joins['']['alias']
2465                : $this->quote($rootModel::getMeta('table'));
2466        }
2467
2468        if (isset($options['table']) && $options['table'])
2469            $field = $alias;
2470        elseif (isset($this->annotations[$field]))
2471            $field = $this->annotations[$field];
2472        elseif ($alias)
2473            $field = $alias.'.'.$this->quote($field);
2474        else
2475            $field = $this->quote($field);
2476
2477        if (isset($options['model']) && $options['model'])
2478            $operator = $model;
2479        return array($field, $operator);
2480    }
2481
2482    /**
2483     * Uses the compiler-specific `compileJoin` function to compile the join
2484     * statement fragment, and caches the result in the local $joins list. A
2485     * new alias is acquired using the `nextAlias` function which will be
2486     * associated with the join. If the same path is requested again, the
2487     * algorithm is short-circuited and the originally-assigned table alias
2488     * is returned immediately.
2489     */
2490    function pushJoin($tip, $path, $model, $info, $constraint=false) {
2491        // TODO: Build the join statement fragment and return the table
2492        // alias. The table alias will be useful where the join is used in
2493        // the WHERE and ORDER BY clauses
2494
2495        // If the join already exists for the statement-being-compiled, just
2496        // return the alias being used.
2497        if (!$constraint && isset($this->joins[$path]))
2498            return $this->joins[$path]['alias'];
2499
2500        // TODO: Support only using aliases if necessary. Use actual table
2501        // names for everything except oddities like self-joins
2502
2503        $alias = $this->nextAlias();
2504        // Keep an association between the table alias and the model. This
2505        // will make model construction much easier when we have the data
2506        // and the table alias from the database.
2507        $this->aliases[$alias] = $model;
2508
2509        // TODO: Stash joins and join constraints into local ->joins array.
2510        // This will be useful metadata in the executor to construct the
2511        // final models for fetching
2512        // TODO: Always use a table alias. This will further help with
2513        // coordination between the data returned from the database (where
2514        // table alias is available) and the corresponding data.
2515        $T = array('alias' => $alias);
2516        $this->joins[$path] = $T;
2517        $this->joins[$path]['sql'] = $this->compileJoin($tip, $model, $alias, $info, $constraint);
2518        return $alias;
2519    }
2520
2521    /**
2522     * compileQ
2523     *
2524     * Build a constraint represented in an arbitrarily nested Q instance.
2525     * The placement of the compiled constraint is also considered and
2526     * represented in the resulting CompiledExpression instance.
2527     *
2528     * Parameters:
2529     * $Q - (Q) constraint represented in a Q instance
2530     * $model - (VerySimpleModel) root model for all the field references in
2531     *      the Q instance
2532     *
2533     * Returns:
2534     * (CompiledExpression) object containing the compiled expression (with
2535     * AND, OR, and NOT operators added). Furthermore, the $type attribute
2536     * of the CompiledExpression will allow the compiler to place the
2537     * constraint properly in the WHERE or HAVING clause appropriately.
2538     */
2539    function compileQ(Q $Q, $model, $parens=true) {
2540        $filter = array();
2541        $type = CompiledExpression::TYPE_WHERE;
2542        foreach ($Q->constraints as $field=>$value) {
2543            $fieldName = $field;
2544            // Handle nested constraints
2545            if ($value instanceof Q) {
2546                $filter[] = $T = $this->compileQ($value, $model,
2547                    !$Q->isCompatibleWith($value));
2548                // Bubble up HAVING constraints
2549                if ($T instanceof CompiledExpression
2550                        && $T->type == CompiledExpression::TYPE_HAVING)
2551                    $type = $T->type;
2552            }
2553            // Handle relationship comparisons with model objects
2554            elseif ($value instanceof VerySimpleModel) {
2555                $criteria = array();
2556                // Avoid a join if possible. Use the local side of the
2557                // relationship
2558                if (count($value->pk) === 1) {
2559                    $path = explode('__', $field);
2560                    $relationship = array_pop($path);
2561                    $lmodel = $model::getMeta()->getByPath($path);
2562                    $local = $lmodel['joins'][$relationship]['local'];
2563                    $path = $path ? (implode('__', $path) . '__') : '';
2564                    foreach ($value->pk as $v) {
2565                        $criteria["{$path}{$local}"] = $v;
2566                   }
2567                }
2568                else {
2569                    foreach ($value->pk as $f=>$v) {
2570                        $criteria["{$field}__{$f}"] = $v;
2571                    }
2572                }
2573                // New criteria here is joined with AND, so if the outer
2574                // criteria is joined with OR, then parentheses are
2575                // necessary
2576                $filter[] = $this->compileQ(new Q($criteria), $model, $Q->ored);
2577            }
2578            // Handle simple field = <value> constraints
2579            else {
2580                list($field, $op) = $this->getField($field, $model);
2581                if ($field instanceof SqlAggregate) {
2582                    // This constraint has to go in the HAVING clause
2583                    $field = $field->toSql($this, $model);
2584                    $type = CompiledExpression::TYPE_HAVING;
2585                } elseif ($field instanceof QuerySet) {
2586                    // Constraint on a subquery goes to HAVING clause
2587                    list($field) = static::splitCriteria($fieldName);
2588                    $type = CompiledExpression::TYPE_HAVING;
2589                }
2590
2591                if ($value === null)
2592                    $filter[] = sprintf('%s IS NULL', $field);
2593                elseif ($value instanceof SqlField)
2594                    $filter[] = sprintf($op, $field, $value->toSql($this, $model));
2595                // Allow operators to be callable rather than sprintf
2596                // strings
2597                elseif (is_callable($op))
2598                    $filter[] = call_user_func($op, $field, $value, $model);
2599                else
2600                    $filter[] = sprintf($op, $field, $this->input($value));
2601            }
2602        }
2603        $glue = $Q->ored ? ' OR ' : ' AND ';
2604        $filter = array_filter($filter);
2605        $clause = implode($glue, $filter);
2606        if (($Q->negated || $parens) && count($filter) > 1)
2607            $clause = '(' . $clause . ')';
2608        if ($Q->negated)
2609            $clause = 'NOT '.$clause;
2610        return new CompiledExpression($clause, $type);
2611    }
2612
2613    function compileConstraints($where, $model) {
2614        $constraints = array();
2615        foreach ($where as $Q) {
2616            // Constraints are joined by AND operators, so if they have
2617            // internal OR operators, then they need to be parenthesized
2618            $constraints[] = $this->compileQ($Q, $model, $Q->ored);
2619        }
2620        return $constraints;
2621    }
2622
2623    function getParams() {
2624        return $this->params;
2625    }
2626
2627    function getJoins($queryset) {
2628        $sql = '';
2629        foreach ($this->joins as $path => $j) {
2630            if (!$j['sql'])
2631                continue;
2632            list($base, $constraints) = $j['sql'];
2633            // Add in path-specific constraints, if any
2634            if (isset($queryset->path_constraints[$path])) {
2635                foreach ($queryset->path_constraints[$path] as $Q) {
2636                    $constraints[] = $this->compileQ($Q, $queryset->model);
2637                }
2638            }
2639            $sql .= $base;
2640            if ($constraints)
2641                $sql .= ' ON ('.implode(' AND ', $constraints).')';
2642        }
2643        // Add extra items from QuerySet
2644        if (isset($queryset->extra['tables'])) {
2645            foreach ($queryset->extra['tables'] as $S) {
2646                $join = ' JOIN ';
2647                // Left joins require an ON () clause
2648                // TODO: Have a way to indicate a LEFT JOIN
2649                $sql .= $join.$S;
2650            }
2651        }
2652
2653        // Add extra joins from QuerySet
2654        if (isset($queryset->extra['joins'])) {
2655            foreach ($queryset->extra['joins'] as $J) {
2656                list($base, $constraints, $alias) = $J;
2657                $join = $constraints ? ' LEFT JOIN ' : ' JOIN ';
2658                $sql .= "{$join}{$base} $alias";
2659                if ($constraints instanceof Q)
2660                    $sql .= ' ON ('.$this->compileQ($constraints, $queryset->model).')';
2661            }
2662        }
2663
2664        return $sql;
2665    }
2666
2667    function nextAlias() {
2668        // Use alias A1-A9,B1-B9,...
2669        $alias = chr(65 + (int)($this->alias_num / 9)) . $this->alias_num % 9;
2670        $this->alias_num++;
2671        return $alias;
2672    }
2673}
2674
2675class CompiledExpression /* extends SplString */ {
2676    const TYPE_WHERE =   0x0001;
2677    const TYPE_HAVING =  0x0002;
2678
2679    var $text = '';
2680
2681    function __construct($clause, $type=self::TYPE_WHERE) {
2682        $this->text = $clause;
2683        $this->type = $type;
2684    }
2685
2686    function __toString() {
2687        return $this->text;
2688    }
2689}
2690
2691class DbEngine {
2692
2693    static $compiler = 'MySqlCompiler';
2694
2695    function __construct($info) {
2696    }
2697
2698    function connect() {
2699    }
2700
2701    // Gets a compiler compatible with this database engine that can compile
2702    // and execute a queryset or DML request.
2703    static function getCompiler() {
2704        $class = static::$compiler;
2705        return new $class();
2706    }
2707
2708    static function delete(VerySimpleModel $model) {
2709        ModelInstanceManager::uncache($model);
2710        return static::getCompiler()->compileDelete($model);
2711    }
2712
2713    static function save(VerySimpleModel $model) {
2714        $compiler = static::getCompiler();
2715        if ($model->__new__)
2716            return $compiler->compileInsert($model);
2717        else
2718            return $compiler->compileUpdate($model);
2719    }
2720}
2721
2722class MySqlCompiler extends SqlCompiler {
2723
2724    static $operators = array(
2725        'exact' => '%1$s = %2$s',
2726        'contains' => array('self', '__contains'),
2727        'startswith' => array('self', '__startswith'),
2728        'endswith' => array('self', '__endswith'),
2729        'gt' => '%1$s > %2$s',
2730        'lt' => '%1$s < %2$s',
2731        'gte' => '%1$s >= %2$s',
2732        'lte' => '%1$s <= %2$s',
2733        'range' => array('self', '__range'),
2734        'isnull' => array('self', '__isnull'),
2735        'like' => '%1$s LIKE %2$s',
2736        'hasbit' => '%1$s & %2$s != 0',
2737        'in' => array('self', '__in'),
2738        'intersect' => array('self', '__find_in_set'),
2739        'regex' => array('self', '__regex'),
2740    );
2741
2742    // Thanks, http://stackoverflow.com/a/3683868
2743    function like_escape($what, $e='\\') {
2744        return str_replace(array($e, '%', '_'), array($e.$e, $e.'%', $e.'_'), $what);
2745    }
2746
2747    function __contains($a, $b) {
2748        # {%a} like %{$b}%
2749        # Escape $b
2750        $b = $this->like_escape($b);
2751        return sprintf('%s LIKE %s', $a, $this->input("%$b%"));
2752    }
2753    function __startswith($a, $b) {
2754        $b = $this->like_escape($b);
2755        return sprintf('%s LIKE %s', $a, $this->input("$b%"));
2756    }
2757    function __endswith($a, $b) {
2758        $b = $this->like_escape($b);
2759        return sprintf('%s LIKE %s', $a, $this->input("%$b"));
2760    }
2761
2762    function __in($a, $b) {
2763        if (is_array($b)) {
2764            $vals = array_map(array($this, 'input'), $b);
2765            $b = '('.implode(', ', $vals).')';
2766        }
2767        // MySQL is almost always faster with a join. Use one if possible
2768        // MySQL doesn't support LIMIT or OFFSET in subqueries. Instead, add
2769        // the query as a JOIN and add the join constraint into the WHERE
2770        // clause.
2771        elseif ($b instanceof QuerySet
2772            && ($b->isWindowed() || $b->countSelectFields() > 1 || $b->chain)
2773        ) {
2774            $f1 = $b->values[0];
2775            $view = $b->asView();
2776            $alias = $this->pushJoin($view, $a, $view, array('constraint'=>array()));
2777            return sprintf('%s = %s.%s', $a, $alias, $this->quote($f1));
2778        }
2779        else {
2780            $b = $this->input($b);
2781        }
2782        return sprintf('%s IN %s', $a, $b);
2783    }
2784
2785    function __isnull($a, $b) {
2786        return $b
2787            ? sprintf('%s IS NULL', $a)
2788            : sprintf('%s IS NOT NULL', $a);
2789    }
2790
2791    function __find_in_set($a, $b) {
2792        if (is_array($b)) {
2793            $sql = array();
2794            foreach (array_map(array($this, 'input'), $b) as $b) {
2795                $sql[] = sprintf('FIND_IN_SET(%s, %s)', $b, $a);
2796            }
2797            $parens = count($sql) > 1;
2798            $sql = implode(' OR ', $sql);
2799            return $parens ? ('('.$sql.')') : $sql;
2800        }
2801        return sprintf('FIND_IN_SET(%s, %s)', $b, $a);
2802    }
2803
2804    function __regex($a, $b) {
2805        // Strip slashes and options
2806        if ($b[0] == '/')
2807            $b = preg_replace('`/[^/]*$`', '', substr($b, 1));
2808        return sprintf('%s REGEXP %s', $a, $this->input($b));
2809    }
2810
2811    function __range($a, $b) {
2812      return sprintf('%s BETWEEN %s AND %s',
2813        $a,
2814        $b[2] ? $b[0] : $this->input($b[0]),
2815        $b[2] ? $b[1] : $this->input($b[1]));
2816    }
2817
2818    function compileJoin($tip, $model, $alias, $info, $extra=false) {
2819        $constraints = array();
2820        $join = ' JOIN ';
2821        if (isset($info['null']) && $info['null'])
2822            $join = ' LEFT'.$join;
2823        if (isset($this->joins[$tip]))
2824            $table = $this->joins[$tip]['alias'];
2825        else
2826            $table = $this->quote($model::getMeta('table'));
2827        foreach ($info['constraint'] as $local => $foreign) {
2828            list($rmodel, $right) = $foreign;
2829            // Support a constant constraint with
2830            // "'constant'" => "Model.field_name"
2831            if ($local[0] == "'") {
2832                $constraints[] = sprintf("%s.%s = %s",
2833                    $alias, $this->quote($right),
2834                    $this->input(trim($local, '\'"'))
2835                );
2836            }
2837            // Support local constraint
2838            // field_name => "'constant'"
2839            elseif ($rmodel[0] == "'" && !$right) {
2840                $constraints[] = sprintf("%s.%s = %s",
2841                    $table, $this->quote($local),
2842                    $this->input(trim($rmodel, '\'"'))
2843                );
2844            }
2845            else {
2846                $constraints[] = sprintf("%s.%s = %s.%s",
2847                    $table, $this->quote($local), $alias,
2848                    $this->quote($right)
2849                );
2850            }
2851        }
2852        // Support extra join constraints
2853        if ($extra instanceof Q) {
2854            $constraints[] = $this->compileQ($extra, $model);
2855        }
2856        if (!isset($rmodel))
2857            $rmodel = $model;
2858        // Support inline views
2859        $rmeta = $rmodel::getMeta();
2860        $table = ($rmeta['view'])
2861            // XXX: Support parameters from the nested query
2862            ? $rmodel::getSqlAddParams($this)
2863            : $this->quote($rmeta['table']);
2864        $base = "{$join}{$table} {$alias}";
2865        return array($base, $constraints);
2866    }
2867
2868    /**
2869     * input
2870     *
2871     * Generate a parameterized input for a database query.
2872     *
2873     * Parameters:
2874     * $what - (mixed) value to be sent to the database. No escaping is
2875     *      necessary. Pass a raw value here.
2876     *
2877     * Returns:
2878     * (string) token to be placed into the compiled SQL statement. This
2879     * is a colon followed by a number
2880     */
2881    function input($what, $model=false) {
2882        if ($what instanceof QuerySet) {
2883            $q = $what->getQuery(array('nosort'=>!($what->limit || $what->offset)));
2884            // Rewrite the parameter numbers so they fit the parameter numbers
2885            // of the current parameters of the $compiler
2886            $self = $this;
2887            $sql = preg_replace_callback("/:(\d+)/",
2888            function($m) use ($self, $q) {
2889                $self->params[] = $q->params[$m[1]-1];
2890                return ':'.count($self->params);
2891            }, $q->sql);
2892            return "({$sql})";
2893        }
2894        elseif ($what instanceof SqlFunction) {
2895            return $what->toSql($this, $model);
2896        }
2897        elseif (!isset($what)) {
2898            return 'NULL';
2899        }
2900        else {
2901            $this->params[] = $what;
2902            return ':'.(count($this->params));
2903        }
2904    }
2905
2906    function quote($what) {
2907        return sprintf("`%s`", str_replace("`", "``", $what));
2908    }
2909
2910    function supportsOption($option) {
2911        return true;
2912    }
2913
2914    /**
2915     * getWhereClause
2916     *
2917     * This builds the WHERE ... part of a DML statement. This should be
2918     * called before ::getJoins(), because it may add joins into the
2919     * statement based on the relationships used in the where clause
2920     */
2921    protected function getWhereHavingClause($queryset) {
2922        $model = $queryset->model;
2923        $constraints = $this->compileConstraints($queryset->constraints, $model);
2924        $where = $having = array();
2925        foreach ($constraints as $C) {
2926            if ($C->type == CompiledExpression::TYPE_WHERE)
2927                $where[] = $C;
2928            else
2929                $having[] = $C;
2930        }
2931        if (isset($queryset->extra['where'])) {
2932            foreach ($queryset->extra['where'] as $S) {
2933                $where[] = "($S)";
2934            }
2935        }
2936        if ($where)
2937            $where = ' WHERE '.implode(' AND ', $where);
2938        if ($having)
2939            $having = ' HAVING '.implode(' AND ', $having);
2940        return array($where ?: '', $having ?: '');
2941    }
2942
2943    function compileCount($queryset) {
2944        $q = clone $queryset;
2945        // Drop extra fields from the queryset
2946        $q->related = $q->anotations = false;
2947        $model = $q->model;
2948        $q->values = $model::getMeta('pk');
2949        $q->annotations = false;
2950        $exec = $q->getQuery(array('nosort' => true));
2951        $exec->sql = 'SELECT COUNT(*) FROM ('.$exec->sql.') __';
2952        $row = $exec->getRow();
2953        return is_array($row) ? (int) $row[0] : null;
2954    }
2955
2956    function getFoundRows() {
2957        $exec = new MysqlExecutor('SELECT FOUND_ROWS()', array());
2958        $row = $exec->getRow();
2959        return is_array($row) ? (int) $row[0] : null;
2960    }
2961
2962    function compileSelect($queryset) {
2963        $model = $queryset->model;
2964        // Use an alias for the root model table
2965        $this->joins[''] = array('alias' => ($rootAlias = $this->nextAlias()));
2966
2967        // Compile the WHERE clause
2968        $this->annotations = $queryset->annotations ?: array();
2969        list($where, $having) = $this->getWhereHavingClause($queryset);
2970
2971        // Compile the ORDER BY clause
2972        $sort = '';
2973        if ($columns = $queryset->getSortFields()) {
2974            $orders = array();
2975            foreach ($columns as $sort) {
2976                $dir = 'ASC';
2977                if (is_array($sort)) {
2978                    list($sort, $dir) = $sort;
2979                }
2980                if ($sort instanceof SqlFunction) {
2981                    $field = $sort->toSql($this, $model);
2982                }
2983                else {
2984                    if ($sort[0] === '-') {
2985                        $dir = 'DESC';
2986                        $sort = substr($sort, 1);
2987                    }
2988                    // If the field is already an annotation, then don't
2989                    // compile the annotation again below. It's included in
2990                    // the select clause, which is sufficient
2991                    if (isset($this->annotations[$sort]))
2992                        $field = $this->quote($sort);
2993                    else
2994                        list($field) = $this->getField($sort, $model);
2995                }
2996                if ($field instanceof SqlFunction)
2997                    $field = $field->toSql($this, $model);
2998                // TODO: Throw exception if $field can be indentified as
2999                //       invalid
3000
3001                $orders[] = "{$field} {$dir}";
3002            }
3003            $sort = ' ORDER BY '.implode(', ', $orders);
3004        }
3005
3006        // Compile the field listing
3007        $fields = $group_by = array();
3008        $meta = $model::getMeta();
3009        $table = $this->quote($meta['table']).' '.$rootAlias;
3010        // Handle related tables
3011        $need_group_by = false;
3012        if ($queryset->related) {
3013            $count = 0;
3014            $fieldMap = $theseFields = array();
3015            $defer = $queryset->defer ?: array();
3016            // Add local fields first
3017            foreach ($meta->getFieldNames() as $f) {
3018                // Handle deferreds
3019                if (isset($defer[$f]))
3020                    continue;
3021                $fields[$rootAlias . '.' . $this->quote($f)] = true;
3022                $theseFields[] = $f;
3023            }
3024            $fieldMap[] = array($theseFields, $model);
3025            // Add the JOINs to this query
3026            foreach ($queryset->related as $sr) {
3027                // XXX: Sort related by the paths so that the shortest paths
3028                //      are resolved first when building out the models.
3029                $full_path = '';
3030                $parts = array();
3031                // Track each model traversal and fetch data for each of the
3032                // models in the path of the related table
3033                foreach (explode('__', $sr) as $field) {
3034                    $full_path .= $field;
3035                    $parts[] = $field;
3036                    $theseFields = array();
3037                    list($alias, $fmodel) = $this->getField($full_path, $model,
3038                        array('table'=>true, 'model'=>true));
3039                    foreach ($fmodel::getMeta()->getFieldNames() as $f) {
3040                        // Handle deferreds
3041                        if (isset($defer[$sr . '__' . $f]))
3042                            continue;
3043                        elseif (isset($fields[$alias.'.'.$this->quote($f)]))
3044                            continue;
3045                        $fields[$alias . '.' . $this->quote($f)] = true;
3046                        $theseFields[] = $f;
3047                    }
3048                    if ($theseFields) {
3049                        $fieldMap[] = array($theseFields, $fmodel, $parts);
3050                    }
3051                    $full_path .= '__';
3052                }
3053            }
3054        }
3055        // Support retrieving only a list of values rather than a model
3056        elseif ($queryset->values) {
3057            $additional_group_by = array();
3058            foreach ($queryset->values as $alias=>$v) {
3059                list($f) = $this->getField($v, $model);
3060                $unaliased = $f;
3061                if ($f instanceof SqlFunction) {
3062                    $fields[$f->toSql($this, $model, $alias)] = true;
3063                    if ($f instanceof SqlAggregate) {
3064                        // Don't group_by aggregate expressions, but if there is an
3065                        // aggergate expression, then we need a GROUP BY clause.
3066                        $need_group_by = true;
3067                        continue;
3068                    }
3069                }
3070                else {
3071                    if (!is_int($alias) && $unaliased != $alias)
3072                        $f .= ' AS '.$this->quote($alias);
3073                    $fields[$f] = true;
3074                }
3075                // If there are annotations, add in these fields to the
3076                // GROUP BY clause
3077                if ($queryset->annotations && !$queryset->distinct)
3078                    $additional_group_by[] = $unaliased;
3079            }
3080            if ($need_group_by && $additional_group_by)
3081                $group_by = array_merge($group_by, $additional_group_by);
3082        }
3083        // Simple selection from one table
3084        elseif (!$queryset->aggregated) {
3085            if ($queryset->defer) {
3086                foreach ($meta->getFieldNames() as $f) {
3087                    if (isset($queryset->defer[$f]))
3088                        continue;
3089                    $fields[$rootAlias .'.'. $this->quote($f)] = true;
3090                }
3091            }
3092            else {
3093                $fields[$rootAlias.'.*'] = true;
3094            }
3095        }
3096        $fields = array_keys($fields);
3097        // Add in annotations
3098        if ($queryset->annotations) {
3099            foreach ($queryset->annotations as $alias=>$A) {
3100                // The root model will receive the annotations, add in the
3101                // annotation after the root model's fields
3102                if ($A instanceof SqlAggregate)
3103                    $need_group_by = true;
3104                $T = $A->toSql($this, $model, $alias);
3105                if ($fieldMap) {
3106                    array_splice($fields, count($fieldMap[0][0]), 0, array($T));
3107                    $fieldMap[0][0][] = $alias;
3108                }
3109                else {
3110                    // No field map — just add to end of field list
3111                    $fields[] = $T;
3112                }
3113            }
3114            // If no group by has been set yet, use the root model pk
3115            if (!$group_by && !$queryset->aggregated && !$queryset->distinct && $need_group_by) {
3116                foreach ($meta['pk'] as $pk)
3117                    $group_by[] = $rootAlias .'.'. $pk;
3118            }
3119        }
3120        // Add in SELECT extras
3121        if (isset($queryset->extra['select'])) {
3122            foreach ($queryset->extra['select'] as $name=>$expr) {
3123                if ($expr instanceof SqlFunction)
3124                    $expr = $expr->toSql($this, false, $name);
3125                else
3126                    $expr = sprintf('%s AS %s', $expr, $this->quote($name));
3127                $fields[] = $expr;
3128            }
3129        }
3130        if (isset($queryset->distinct)) {
3131            foreach ($queryset->distinct as $d)
3132                list($group_by[]) = $this->getField($d, $model);
3133        }
3134        $group_by = $group_by ? ' GROUP BY '.implode(', ', $group_by) : '';
3135
3136        $joins = $this->getJoins($queryset);
3137        if ($hint = $queryset->getOption(QuerySet::OPT_INDEX_HINT)) {
3138            $hint = " USE INDEX ({$hint})";
3139        }
3140
3141        $sql = 'SELECT ';
3142        if ($queryset->hasOption(QuerySet::OPT_MYSQL_FOUND_ROWS))
3143            $sql .= 'SQL_CALC_FOUND_ROWS ';
3144        $sql .= implode(', ', $fields).' FROM '
3145            .$table.$hint.$joins.$where.$group_by.$having.$sort;
3146        // UNIONS
3147        if ($queryset->chain) {
3148            // If the main query is sorted, it will need parentheses
3149            if ($parens = (bool) $sort)
3150                $sql = "($sql)";
3151            foreach ($queryset->chain as $qs) {
3152                list($qs, $all) = $qs;
3153                $q = $qs->getQuery(array('nosort' => true));
3154                // Rewrite the parameter numbers so they fit the parameter numbers
3155                // of the current parameters of the $compiler
3156                $self = $this;
3157                $S = preg_replace_callback("/:(\d+)/",
3158                function($m) use ($self, $q) {
3159                    $self->params[] = $q->params[$m[1]-1];
3160                    return ':'.count($self->params);
3161                }, $q->sql);
3162                // Wrap unions in parentheses if they are windowed or sorted
3163                if ($parens || $qs->isWindowed() || count($qs->getSortFields()))
3164                    $S = "($S)";
3165                $sql .= ' UNION '.($all ? 'ALL ' : '').$S;
3166            }
3167        }
3168
3169        if ($queryset->limit)
3170            $sql .= ' LIMIT '.$queryset->limit;
3171        if ($queryset->offset)
3172            $sql .= ' OFFSET '.$queryset->offset;
3173        switch ($queryset->lock) {
3174        case QuerySet::LOCK_EXCLUSIVE:
3175            $sql .= ' FOR UPDATE';
3176            break;
3177        case QuerySet::LOCK_SHARED:
3178            $sql .= ' LOCK IN SHARE MODE';
3179            break;
3180        }
3181
3182        return new MysqlExecutor($sql, $this->params, $fieldMap);
3183    }
3184
3185    function __compileUpdateSet($model, array $pk) {
3186        $fields = array();
3187        foreach ($model->dirty as $field=>$old) {
3188            if ($model->__new__ or !in_array($field, $pk)) {
3189                $fields[] = sprintf('%s = %s', $this->quote($field),
3190                    $this->input($model->get($field)));
3191            }
3192        }
3193        return ' SET '.implode(', ', $fields);
3194    }
3195
3196    function compileUpdate(VerySimpleModel $model) {
3197        $pk = $model::getMeta('pk');
3198        $sql = 'UPDATE '.$this->quote($model::getMeta('table'));
3199        $sql .= $this->__compileUpdateSet($model, $pk);
3200        // Support PK updates
3201        $criteria = array();
3202        foreach ($pk as $f) {
3203            $criteria[$f] = @$model->dirty[$f] ?: $model->get($f);
3204        }
3205        $sql .= ' WHERE '.$this->compileQ(new Q($criteria), $model);
3206        $sql .= ' LIMIT 1';
3207
3208        return new MySqlExecutor($sql, $this->params);
3209    }
3210
3211    function compileInsert(VerySimpleModel $model) {
3212        $pk = $model::getMeta('pk');
3213        $sql = 'INSERT INTO '.$this->quote($model::getMeta('table'));
3214        $sql .= $this->__compileUpdateSet($model, $pk);
3215
3216        return new MySqlExecutor($sql, $this->params);
3217    }
3218
3219    function compileDelete($model) {
3220        $table = $model::getMeta('table');
3221
3222        $where = ' WHERE '.implode(' AND ',
3223            $this->compileConstraints(array(new Q($model->pk)), $model));
3224        $sql = 'DELETE FROM '.$this->quote($table).$where.' LIMIT 1';
3225        return new MySqlExecutor($sql, $this->params);
3226    }
3227
3228    function compileBulkDelete($queryset) {
3229        $model = $queryset->model;
3230        $table = $model::getMeta('table');
3231        list($where, $having) = $this->getWhereHavingClause($queryset);
3232        $joins = $this->getJoins($queryset);
3233        $sql = 'DELETE '.$this->quote($table).'.* FROM '
3234            .$this->quote($table).$joins.$where;
3235        return new MysqlExecutor($sql, $this->params);
3236    }
3237
3238    function compileBulkUpdate($queryset, array $what) {
3239        $model = $queryset->model;
3240        $table = $model::getMeta('table');
3241        $set = array();
3242        foreach ($what as $field=>$value)
3243            $set[] = sprintf('%s = %s', $this->quote($field), $this->input($value, $model));
3244        $set = implode(', ', $set);
3245        list($where, $having) = $this->getWhereHavingClause($queryset);
3246        $joins = $this->getJoins($queryset);
3247        $sql = 'UPDATE '.$this->quote($table).$joins.' SET '.$set.$where;
3248        return new MysqlExecutor($sql, $this->params);
3249    }
3250
3251    // Returns meta data about the table used to build queries
3252    function inspectTable($table) {
3253        static $cache = array();
3254
3255        // XXX: Assuming schema is not changing — add support to track
3256        //      current schema
3257        if (isset($cache[$table]))
3258            return $cache[$table];
3259
3260        $sql = 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS '
3261            .'WHERE TABLE_NAME = '.db_input($table).' AND TABLE_SCHEMA = DATABASE() '
3262            .'ORDER BY ORDINAL_POSITION';
3263        $ex = new MysqlExecutor($sql, array());
3264        $columns = array();
3265        while (list($column) = $ex->getRow()) {
3266            $columns[] = $column;
3267        }
3268        return $cache[$table] = $columns;
3269    }
3270}
3271
3272class MySqlPreparedExecutor {
3273
3274    var $stmt;
3275    var $fields = array();
3276    var $sql;
3277    var $params;
3278    // Array of [count, model] values representing which fields in the
3279    // result set go with witch model.  Useful for handling select_related
3280    // queries
3281    var $map;
3282
3283    var $unbuffered = false;
3284
3285    function __construct($sql, $params, $map=null) {
3286        $this->sql = $sql;
3287        $this->params = $params;
3288        $this->map = $map;
3289    }
3290
3291    function getMap() {
3292        return $this->map;
3293    }
3294
3295    function setBuffered($buffered) {
3296        $this->unbuffered = !$buffered;
3297    }
3298
3299    function fixupParams() {
3300        $self = $this;
3301        $params = array();
3302        $sql = preg_replace_callback("/:(\d+)/",
3303        function($m) use ($self, &$params) {
3304            $params[] = $self->params[$m[1]-1];
3305            return '?';
3306        }, $this->sql);
3307        return array($sql, $params);
3308    }
3309
3310    function _prepare() {
3311        $this->execute();
3312        $this->_setup_output();
3313    }
3314
3315    function execute() {
3316        list($sql, $params) = $this->fixupParams();
3317        if (!($this->stmt = db_prepare($sql)))
3318            throw new InconsistentModelException(
3319                'Unable to prepare query: '.db_error().' '.$sql);
3320        if (count($params))
3321            $this->_bind($params);
3322        if (!$this->stmt->execute() || !($this->unbuffered || $this->stmt->store_result())) {
3323            throw new OrmException('Unable to execute query: ' . $this->stmt->error);
3324        }
3325        return true;
3326    }
3327
3328    function _bind($params) {
3329        if (count($params) != $this->stmt->param_count)
3330            throw new Exception(__('Parameter count does not match query'));
3331
3332        $types = '';
3333        $ps = array();
3334        foreach ($params as $i=>&$p) {
3335            if (is_int($p) || is_bool($p))
3336                $types .= 'i';
3337            elseif (is_float($p))
3338                $types .= 'd';
3339            elseif (is_string($p))
3340                $types .= 's';
3341            elseif ($p instanceof DateTime) {
3342                $types .= 's';
3343                $p = $p->format('Y-m-d H:i:s');
3344            }
3345            elseif (is_object($p)) {
3346                $types .= 's';
3347                $p = (string) $p;
3348            }
3349            // TODO: Emit error if param is null
3350            $ps[] = &$p;
3351        }
3352        unset($p);
3353        array_unshift($ps, $types);
3354        call_user_func_array(array($this->stmt,'bind_param'), $ps);
3355    }
3356
3357    function _setup_output() {
3358        if (!($meta = $this->stmt->result_metadata()))
3359            throw new OrmException('Unable to fetch statment metadata: ', $this->stmt->error);
3360        $this->fields = $meta->fetch_fields();
3361        $meta->free_result();
3362    }
3363
3364    // Iterator interface
3365    function rewind() {
3366        if (!isset($this->stmt))
3367            $this->_prepare();
3368        $this->stmt->data_seek(0);
3369    }
3370
3371    function next() {
3372        $status = $this->stmt->fetch();
3373        if ($status === false)
3374            throw new OrmException($this->stmt->error);
3375        elseif ($status === null) {
3376            $this->close();
3377            return false;
3378        }
3379        return true;
3380    }
3381
3382    function getArray() {
3383        $output = array();
3384        $variables = array();
3385
3386        if (!isset($this->stmt))
3387            $this->_prepare();
3388
3389        foreach ($this->fields as $f)
3390            $variables[] = &$output[$f->name]; // pass by reference
3391
3392        if (!call_user_func_array(array($this->stmt, 'bind_result'), $variables))
3393            throw new OrmException('Unable to bind result: ' . $this->stmt->error);
3394
3395        if (!$this->next())
3396            return false;
3397        return $output;
3398    }
3399
3400    function getRow() {
3401        $output = array();
3402        $variables = array();
3403
3404        if (!isset($this->stmt))
3405            $this->_prepare();
3406
3407        foreach ($this->fields as $f)
3408            $variables[] = &$output[]; // pass by reference
3409
3410        if (!call_user_func_array(array($this->stmt, 'bind_result'), $variables))
3411            throw new OrmException('Unable to bind result: ' . $this->stmt->error);
3412
3413        if (!$this->next())
3414            return false;
3415        return $output;
3416    }
3417
3418    function close() {
3419        if (!$this->stmt)
3420            return;
3421
3422        $this->stmt->close();
3423        $this->stmt = null;
3424    }
3425
3426    function affected_rows() {
3427        return $this->stmt->affected_rows;
3428    }
3429
3430    function insert_id() {
3431        return $this->stmt->insert_id;
3432    }
3433
3434    function __toString() {
3435        $self = $this;
3436        return preg_replace_callback("/:(\d+)(?=([^']*'[^']*')*[^']*$)/",
3437        function($m) use ($self) {
3438            $p = $self->params[$m[1]-1];
3439            switch (true) {
3440            case is_bool($p):
3441                $p = (int) $p;
3442            case is_int($p):
3443            case is_float($p):
3444                return $p;
3445            case $p instanceof DateTime:
3446                $p = $p->format('Y-m-d H:i:s');
3447            default:
3448                return db_real_escape((string) $p, true);
3449           }
3450        }, $this->sql);
3451    }
3452}
3453
3454/**
3455 * Simplified executor which uses the mysqli_query() function to process
3456 * queries. This method is faster on MySQL as it doesn't require the PREPARE
3457 * overhead, nor require two trips to the database per query. All parameters
3458 * are escaped and placed directly into the SQL statement. With this style,
3459 * it is possible that multiple parameters could compile a statement which
3460 * exceeds the MySQL max_allowed_packet setting.
3461 */
3462class MySqlExecutor
3463extends MySqlPreparedExecutor {
3464    function execute() {
3465        $sql = $this->__toString();
3466        if (!($this->stmt = db_query($sql, true, !$this->unbuffered)))
3467            throw new InconsistentModelException(
3468                'Unable to prepare query: '.db_error().' '.$sql);
3469        // mysqli_query() return TRUE for UPDATE queries and friends
3470        if ($this->stmt !== true)
3471            $this->_setupCast();
3472        return true;
3473    }
3474
3475    function _setupCast() {
3476        $fields = $this->stmt->fetch_fields();
3477        $this->types = array();
3478        foreach ($fields as $F) {
3479            $this->types[] = $F->type;
3480        }
3481    }
3482
3483    function _cast($record) {
3484        $i=0;
3485        foreach ($record as &$f) {
3486            switch ($this->types[$i++]) {
3487            case MYSQLI_TYPE_DECIMAL:
3488            case MYSQLI_TYPE_NEWDECIMAL:
3489            case MYSQLI_TYPE_LONGLONG:
3490            case MYSQLI_TYPE_FLOAT:
3491            case MYSQLI_TYPE_DOUBLE:
3492                $f = isset($f) ? (double) $f : $f;
3493                break;
3494
3495            case MYSQLI_TYPE_BIT:
3496            case MYSQLI_TYPE_TINY:
3497            case MYSQLI_TYPE_SHORT:
3498            case MYSQLI_TYPE_LONG:
3499            case MYSQLI_TYPE_INT24:
3500                $f = isset($f) ? (int) $f : $f;
3501                break;
3502
3503            default:
3504                // No change (leave as string)
3505            }
3506        }
3507        unset($f);
3508        return $record;
3509    }
3510
3511    function getArray() {
3512        if (!isset($this->stmt))
3513            $this->execute();
3514
3515        if (null === ($record = $this->stmt->fetch_assoc()))
3516            return false;
3517        return $this->_cast($record);
3518    }
3519
3520    function getRow() {
3521        if (!isset($this->stmt))
3522            $this->execute();
3523
3524        if (null === ($record = $this->stmt->fetch_row()))
3525            return false;
3526        return $this->_cast($record);
3527    }
3528
3529    function affected_rows() {
3530        return db_affected_rows();
3531    }
3532
3533    function insert_id() {
3534        return db_insert_id();
3535    }
3536}
3537
3538class Q implements Serializable {
3539    const NEGATED = 0x0001;
3540    const ANY =     0x0002;
3541
3542    var $constraints;
3543    var $negated = false;
3544    var $ored = false;
3545
3546    function __construct($filter=array(), $flags=0) {
3547        if (!is_array($filter))
3548            $filter = array($filter);
3549        $this->constraints = $filter;
3550        $this->negated = $flags & self::NEGATED;
3551        $this->ored = $flags & self::ANY;
3552    }
3553
3554    function isNegated() {
3555        return $this->negated;
3556    }
3557
3558    function isOred() {
3559        return $this->ored;
3560    }
3561
3562    function negate() {
3563        $this->negated = !$this->negated;
3564        return $this;
3565    }
3566
3567    function union() {
3568        $this->ored = true;
3569    }
3570
3571    /**
3572     * Two neighboring Q's are compatible in a where clause if they have
3573     * the same boolean AND / OR operator. Negated Q's should always use
3574     * parentheses if there is more than one criterion.
3575     */
3576    function isCompatibleWith(Q $other) {
3577        return $this->ored == $other->ored;
3578    }
3579
3580    function add($constraints) {
3581        if (is_array($constraints))
3582            $this->constraints = array_merge($this->constraints, $constraints);
3583        elseif ($constraints instanceof static)
3584            $this->constraints[] = $constraints;
3585        else
3586            throw new InvalidArgumentException('Expected an instance of Q or an array thereof');
3587        return $this;
3588    }
3589
3590    static function not($constraints) {
3591        return new static($constraints, self::NEGATED);
3592    }
3593
3594    static function any($constraints) {
3595        return new static($constraints, self::ANY);
3596    }
3597
3598    static function all($constraints) {
3599        return new static($constraints);
3600    }
3601
3602    function evaluate($record) {
3603        // Start with FALSE for OR and TRUE for AND
3604        $result = !$this->ored;
3605        foreach ($this->constraints as $field=>$check) {
3606            $R = SqlCompiler::evaluate($record, $check, $field);
3607            if ($this->ored) {
3608                if ($result |= $R)
3609                    break;
3610            }
3611            elseif (!$R) {
3612                // Anything AND false
3613                $result = false;
3614                break;
3615            }
3616        }
3617        if ($this->negated)
3618            $result = !$result;
3619        return $result;
3620    }
3621
3622    function serialize() {
3623        return serialize(array($this->negated, $this->ored, $this->constraints));
3624    }
3625
3626    function unserialize($data) {
3627        list($this->negated, $this->ored, $this->constraints) = unserialize($data);
3628    }
3629}
3630?>
3631