Skip to content
Snippets Groups Projects
class.orm.php 92.9 KiB
Newer Older
  • Learn to ignore specific revisions
  •                     $field = $field->toSql($this, $model);
                        $type = CompiledExpression::TYPE_HAVING;
                    }
    
                    if ($value === null)
                        $filter[] = sprintf('%s IS NULL', $field);
    
                    elseif ($value instanceof SqlField)
                        $filter[] = sprintf($op, $field, $value->toSql($this, $model));
    
                    // Allow operators to be callable rather than sprintf
                    // strings
    
                    elseif (is_callable($op))
    
                        $filter[] = call_user_func($op, $field, $value, $model);
    
    Jared Hancock's avatar
    Jared Hancock committed
                    else
    
                        $filter[] = sprintf($op, $field, $this->input($value, $slot));
    
            $glue = $Q->isOred() ? ' OR ' : ' AND ';
            $clause = implode($glue, $filter);
            if (count($filter) > 1)
                $clause = '(' . $clause . ')';
            if ($Q->isNegated())
    
                $clause = 'NOT '.$clause;
    
            return new CompiledExpression($clause, $type);
    
        }
    
        function compileConstraints($where, $model) {
            $constraints = array();
            foreach ($where as $Q) {
                $constraints[] = $this->compileQ($Q, $model);
            }
    
            return $constraints;
    
        }
    
        function getParams() {
            return $this->params;
        }
    
    
        function getJoins($queryset) {
    
            foreach ($this->joins as $path => $j) {
                if (!$j['sql'])
                    continue;
                list($base, $constraints) = $j['sql'];
                // Add in path-specific constraints, if any
                if (isset($queryset->path_constraints[$path])) {
                    foreach ($queryset->path_constraints[$path] as $Q) {
                        $constraints[] = $this->compileQ($Q, $queryset->model);
                    }
                }
                $sql .= $base;
                if ($constraints)
                    $sql .= ' ON ('.implode(' AND ', $constraints).')';
            }
    
            // Add extra items from QuerySet
            if (isset($queryset->extra['tables'])) {
                foreach ($queryset->extra['tables'] as $S) {
                    $join = ' JOIN ';
                    // Left joins require an ON () clause
                    if ($lastparen = strrpos($S, '(')) {
                        if (preg_match('/\bon\b/i', substr($S, $lastparen - 4, 4)))
                            $join = ' LEFT' . $join;
                    }
                    $sql .= $join.$S;
                }
            }
    
            return $sql;
        }
    
        function nextAlias() {
            // Use alias A1-A9,B1-B9,...
            $alias = chr(65 + (int)($this->alias_num / 9)) . $this->alias_num % 9;
            $this->alias_num++;
            return $alias;
        }
    }
    
    
    class CompiledExpression /* extends SplString */ {
        const TYPE_WHERE =   0x0001;
        const TYPE_HAVING =  0x0002;
    
        var $text = '';
    
        function __construct($clause, $type=self::TYPE_WHERE) {
            $this->text = $clause;
            $this->type = $type;
        }
    
        function __toString() {
            return $this->text;
        }
    }
    
    
        static $compiler = 'MySqlCompiler';
    
    
        function __construct($info) {
        }
    
        function connect() {
        }
    
        // Gets a compiler compatible with this database engine that can compile
        // and execute a queryset or DML request.
    
        static function getCompiler() {
            $class = static::$compiler;
            return new $class();
    
    
        static function delete(VerySimpleModel $model) {
    
            ModelInstanceManager::uncache($model);
    
            return static::getCompiler()->compileDelete($model);
    
        }
    
        static function save(VerySimpleModel $model) {
    
            $compiler = static::getCompiler();
    
            if ($model->__new__)
                return $compiler->compileInsert($model);
            else
                return $compiler->compileUpdate($model);
        }
    
    }
    
    class MySqlCompiler extends SqlCompiler {
    
        static $operators = array(
            'exact' => '%1$s = %2$s',
            'contains' => array('self', '__contains'),
    
            'startswith' => array('self', '__startswith'),
    
            'endswith' => array('self', '__endswith'),
    
            'gt' => '%1$s > %2$s',
            'lt' => '%1$s < %2$s',
    
            'gte' => '%1$s >= %2$s',
            'lte' => '%1$s <= %2$s',
    
            'isnull' => array('self', '__isnull'),
    
            'like' => '%1$s LIKE %2$s',
    
            'hasbit' => '%1$s & %2$s != 0',
    
            'in' => array('self', '__in'),
    
            'intersect' => array('self', '__find_in_set'),
    
        // Thanks, http://stackoverflow.com/a/3683868
        function like_escape($what, $e='\\') {
            return str_replace(array($e, '%', '_'), array($e.$e, $e.'%', $e.'_'), $what);
        }
    
    
        function __contains($a, $b) {
            # {%a} like %{$b}%
    
            # Escape $b
            $b = $this->like_escape($b);
            return sprintf('%s LIKE %s', $a, $this->input("%$b%"));
        }
        function __startswith($a, $b) {
            $b = $this->like_escape($b);
    
            return sprintf('%s LIKE %s', $a, $this->input("$b%"));
    
        }
        function __endswith($a, $b) {
            $b = $this->like_escape($b);
    
            return sprintf('%s LIKE %s', $a, $this->input("%$b"));
    
        }
    
        function __in($a, $b) {
            if (is_array($b)) {
                $vals = array_map(array($this, 'input'), $b);
    
                $b = '('.implode(', ', $vals).')';
    
            // MySQL doesn't support LIMIT or OFFSET in subqueries. Instead, add
    
            // the query as a JOIN and add the join constraint into the WHERE
            // clause.
    
            elseif ($b instanceof QuerySet && $b->isWindowed()) {
                $f1 = $b->values[0];
                $view = $b->asView();
                $alias = $this->pushJoin($view, $a, $view, array('constraint'=>array()));
                return sprintf('%s = %s.%s', $a, $alias, $this->quote($f1));
            }
    
            else {
                $b = $this->input($b);
            }
    
            return sprintf('%s IN %s', $a, $b);
    
        function __isnull($a, $b) {
            return $b
                ? sprintf('%s IS NULL', $a)
                : sprintf('%s IS NOT NULL', $a);
        }
    
    
        function __find_in_set($a, $b) {
            if (is_array($b)) {
                $sql = array();
                foreach (array_map(array($this, 'input'), $b) as $b) {
                    $sql[] = sprintf('FIND_IN_SET(%s, %s)', $b, $a);
                }
                $parens = count($sql) > 1;
                $sql = implode(' OR ', $sql);
                return $parens ? ('('.$sql.')') : $sql;
            }
            return sprintf('FIND_IN_SET(%s, %s)', $b, $a);
        }
    
    
        function compileJoin($tip, $model, $alias, $info, $extra=false) {
    
            $constraints = array();
            $join = ' JOIN ';
            if (isset($info['null']) && $info['null'])
                $join = ' LEFT'.$join;
            if (isset($this->joins[$tip]))
                $table = $this->joins[$tip]['alias'];
            else
    
                $table = $this->quote($model::getMeta('table'));
    
            foreach ($info['constraint'] as $local => $foreign) {
    
                list($rmodel, $right) = $foreign;
    
                // Support a constant constraint with
                // "'constant'" => "Model.field_name"
                if ($local[0] == "'") {
                    $constraints[] = sprintf("%s.%s = %s",
                        $alias, $this->quote($right),
    
                        $this->input(trim($local, '\'"'))
    
                // Support local constraint
                // field_name => "'constant'"
    
                elseif ($rmodel[0] == "'" && !$right) {
    
                    $constraints[] = sprintf("%s.%s = %s",
                        $table, $this->quote($local),
    
                        $this->input(trim($rmodel, '\'"'))
    
                else {
                    $constraints[] = sprintf("%s.%s = %s.%s",
                        $table, $this->quote($local), $alias,
                        $this->quote($right)
                    );
                }
    
            // Support extra join constraints
            if ($extra instanceof Q) {
    
                $constraints[] = $this->compileQ($extra, $model);
    
            if (!isset($rmodel))
                $rmodel = $model;
    
            // Support inline views
    
            $table = ($rmodel::getMeta('view'))
    
                // XXX: Support parameters from the nested query
    
                ? $rmodel::getSqlAddParams($this)
    
                : $this->quote($rmodel::getMeta('table'));
    
            $base = "{$join}{$table} {$alias}";
    
            return array($base, $constraints);
    
         * Generate a parameterized input for a database query.
    
         *
         * Parameters:
         * $what - (mixed) value to be sent to the database. No escaping is
         *      necessary. Pass a raw value here.
         *
         * Returns:
    
         * (string) token to be placed into the compiled SQL statement. This
         * is a colon followed by a number
    
        function input($what, $slot=false, $model=false) {
    
            if ($what instanceof QuerySet) {
                $q = $what->getQuery(array('nosort'=>true));
    
                // Rewrite the parameter numbers so they fit the parameter numbers
                // of the current parameters of the $compiler
                $self = $this;
                $sql = preg_replace_callback("/:(\d+)/",
                function($m) use ($self, $q) {
                    $self->params[] = $q->params[$m[1]-1];
                    return ':'.count($self->params);
                }, $q->sql);
                return "({$sql})";
    
            elseif ($what instanceof SqlFunction) {
    
                return $what->toSql($this, $model);
    
            elseif (!isset($what)) {
                return 'NULL';
            }
    
                $this->params[] = $what;
                return ':'.(count($this->params));
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function quote($what) {
            return "`$what`";
        }
    
    
        /**
         * getWhereClause
         *
         * This builds the WHERE ... part of a DML statement. This should be
         * called before ::getJoins(), because it may add joins into the
         * statement based on the relationships used in the where clause
         */
    
        protected function getWhereHavingClause($queryset) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            $model = $queryset->model;
    
            $constraints = $this->compileConstraints($queryset->constraints, $model);
            $where = $having = array();
            foreach ($constraints as $C) {
                if ($C->type == CompiledExpression::TYPE_WHERE)
                    $where[] = $C;
                else
                    $having[] = $C;
            }
    
            if (isset($queryset->extra['where'])) {
                foreach ($queryset->extra['where'] as $S) {
    
                $where = ' WHERE '.implode(' AND ', $where);
            if ($having)
                $having = ' HAVING '.implode(' AND ', $having);
            return array($where ?: '', $having ?: '');
    
        }
    
        function compileCount($queryset) {
            $model = $queryset->model;
    
            $table = $model::getMeta('table');
    
            list($where, $having) = $this->getWhereHavingClause($queryset);
    
            $joins = $this->getJoins($queryset);
    
    Jared Hancock's avatar
    Jared Hancock committed
            $sql = 'SELECT COUNT(*) AS count FROM '.$this->quote($table).$joins.$where;
            $exec = new MysqlExecutor($sql, $this->params);
            $row = $exec->getArray();
            return $row['count'];
        }
    
        function compileSelect($queryset) {
            $model = $queryset->model;
    
            // Use an alias for the root model table
            $this->joins[''] = array('alias' => ($rootAlias = $this->nextAlias()));
    
            // Compile the WHERE clause
    
            $this->annotations = $queryset->annotations ?: array();
            list($where, $having) = $this->getWhereHavingClause($queryset);
    
            // Compile the ORDER BY clause
    
    Jared Hancock's avatar
    Jared Hancock committed
            $sort = '';
    
            if ($columns = $queryset->getSortFields()) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                $orders = array();
    
                foreach ($columns as $sort) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                    $dir = 'ASC';
    
                    if ($sort instanceof SqlFunction) {
                        $field = $sort->toSql($this, $model);
                    }
                    else {
                        if ($sort[0] == '-') {
                            $dir = 'DESC';
                            $sort = substr($sort, 1);
                        }
    
                        // If the field is already an annotation, then don't
                        // compile the annotation again below. It's included in
                        // the select clause, which is sufficient
                        if (isset($this->annotations[$sort]))
                            $field = $this->quote($sort);
                        else
                            list($field) = $this->getField($sort, $model);
    
    Peter Rotich's avatar
    Peter Rotich committed
                    if ($field instanceof SqlFunction)
                        $field = $field->toSql($this, $model);
    
                    // TODO: Throw exception if $field can be indentified as
                    //       invalid
    
    Peter Rotich's avatar
    Peter Rotich committed
    
    
                    $orders[] = "{$field} {$dir}";
    
    Jared Hancock's avatar
    Jared Hancock committed
                }
                $sort = ' ORDER BY '.implode(', ', $orders);
            }
    
    
            // Compile the field listing
    
            $fields = $group_by = array();
            $table = $this->quote($model::getMeta('table')).' '.$rootAlias;
    
            // Handle related tables
    
    Jared Hancock's avatar
    Jared Hancock committed
            if ($queryset->related) {
    
                $fieldMap = $theseFields = array();
    
                $defer = $queryset->defer ?: array();
                // Add local fields first
    
                foreach ($model::getMeta('fields') as $f) {
    
                    // Handle deferreds
                    if (isset($defer[$f]))
                        continue;
    
                    $fields[$rootAlias . '.' . $this->quote($f)] = true;
    
                    $theseFields[] = $f;
    
                $fieldMap[] = array($theseFields, $model);
    
                // Add the JOINs to this query
                foreach ($queryset->related as $sr) {
    
                    // XXX: Sort related by the paths so that the shortest paths
                    //      are resolved first when building out the models.
    
                    $full_path = '';
                    $parts = array();
                    // Track each model traversal and fetch data for each of the
                    // models in the path of the related table
                    foreach (explode('__', $sr) as $field) {
                        $full_path .= $field;
                        $parts[] = $field;
    
                        $theseFields = array();
    
                        list($alias, $fmodel) = $this->getField($full_path, $model,
                            array('table'=>true, 'model'=>true));
    
                        foreach ($fmodel::getMeta('fields') as $f) {
    
                            // Handle deferreds
                            if (isset($defer[$sr . '__' . $f]))
                                continue;
    
                            elseif (isset($fields[$alias.'.'.$this->quote($f)]))
                                continue;
                            $fields[$alias . '.' . $this->quote($f)] = true;
    
                            $theseFields[] = $f;
    
                        if ($theseFields) {
                            $fieldMap[] = array($theseFields, $fmodel, $parts);
                        }
    
                        $full_path .= '__';
                    }
                }
            }
            // Support retrieving only a list of values rather than a model
            elseif ($queryset->values) {
    
                foreach ($queryset->values as $alias=>$v) {
    
                    list($f) = $this->getField($v, $model);
    
                    if ($f instanceof SqlFunction)
    
                        $fields[$f->toSql($this, $model, $alias)] = true;
                    else {
                        if (!is_int($alias))
                            $f .= ' AS '.$this->quote($alias);
    
                        $fields[$f] = true;
    
                    // If there are annotations, add in these fields to the
                    // GROUP BY clause
                    if ($queryset->annotations)
                        $group_by[] = $unaliased;
    
            }
            // Simple selection from one table
    
            elseif (!$queryset->aggregated) {
    
                    foreach ($model::getMeta('fields') as $f) {
    
                        if (isset($queryset->defer[$f]))
                            continue;
    
                        $fields[$rootAlias .'.'. $this->quote($f)] = true;
    
                    $fields[$rootAlias.'.*'] = true;
    
            $fields = array_keys($fields);
    
            // Add in annotations
            if ($queryset->annotations) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                foreach ($queryset->annotations as $alias=>$A) {
    
                    // The root model will receive the annotations, add in the
                    // annotation after the root model's fields
                    $T = $A->toSql($this, $model, $alias);
                    if ($fieldMap) {
                        array_splice($fields, count($fieldMap[0][0]), 0, array($T));
    
                        $fieldMap[0][0][] = $A->getAlias();
    
                    }
                    else {
                        // No field map — just add to end of field list
                        $fields[] = $T;
                    }
    
                // If no group by has been set yet, use the root model pk
    
                if (!$group_by && !$queryset->aggregated) {
    
                    foreach ($model::getMeta('pk') as $pk)
    
                        $group_by[] = $rootAlias .'.'. $pk;
                }
    
            // Add in SELECT extras
            if (isset($queryset->extra['select'])) {
                foreach ($queryset->extra['select'] as $name=>$expr) {
                    if ($expr instanceof SqlFunction)
                        $expr = $expr->toSql($this, false, $name);
                    $fields[] = $expr;
                }
            }
    
            if (isset($queryset->distinct)) {
                foreach ($queryset->distinct as $d)
                    list($group_by[]) = $this->getField($d, $model);
            }
    
            $group_by = $group_by ? ' GROUP BY '.implode(', ', $group_by) : '';
    
    
            $joins = $this->getJoins($queryset);
    
    Jared Hancock's avatar
    Jared Hancock committed
    
            $sql = 'SELECT '.implode(', ', $fields).' FROM '
    
                .$table.$joins.$where.$group_by.$having.$sort;
    
    Jared Hancock's avatar
    Jared Hancock committed
            if ($queryset->limit)
                $sql .= ' LIMIT '.$queryset->limit;
            if ($queryset->offset)
                $sql .= ' OFFSET '.$queryset->offset;
    
            switch ($queryset->lock) {
            case QuerySet::LOCK_EXCLUSIVE:
                $sql .= ' FOR UPDATE';
                break;
            case QuerySet::LOCK_SHARED:
                $sql .= ' LOCK IN SHARE MODE';
                break;
            }
    
            return new MysqlExecutor($sql, $this->params, $fieldMap);
    
        function __compileUpdateSet($model, array $pk) {
            $fields = array();
            foreach ($model->dirty as $field=>$old) {
                if ($model->__new__ or !in_array($field, $pk)) {
                    $fields[] = sprintf('%s = %s', $this->quote($field),
                        $this->input($model->get($field)));
                }
            }
            return ' SET '.implode(', ', $fields);
        }
    
        function compileUpdate(VerySimpleModel $model) {
    
            $pk = $model::getMeta('pk');
            $sql = 'UPDATE '.$this->quote($model::getMeta('table'));
    
            $sql .= $this->__compileUpdateSet($model, $pk);
    
            // Support PK updates
            $criteria = array();
            foreach ($pk as $f) {
                $criteria[$f] = @$model->dirty[$f] ?: $model->get($f);
            }
            $sql .= ' WHERE '.$this->compileQ(new Q($criteria), $model);
    
            $sql .= ' LIMIT 1';
    
            return new MySqlExecutor($sql, $this->params);
        }
    
        function compileInsert(VerySimpleModel $model) {
    
            $pk = $model::getMeta('pk');
            $sql = 'INSERT INTO '.$this->quote($model::getMeta('table'));
    
            $sql .= $this->__compileUpdateSet($model, $pk);
    
            return new MySqlExecutor($sql, $this->params);
    
        function compileDelete($model) {
    
            $table = $model::getMeta('table');
    
            $where = ' WHERE '.implode(' AND ',
                $this->compileConstraints(array(new Q($model->pk)), $model));
    
            $sql = 'DELETE FROM '.$this->quote($table).$where.' LIMIT 1';
            return new MySqlExecutor($sql, $this->params);
    
        function compileBulkDelete($queryset) {
    
            $model = $queryset->model;
    
            $table = $model::getMeta('table');
    
            list($where, $having) = $this->getWhereHavingClause($queryset);
    
            $joins = $this->getJoins($queryset);
    
            $sql = 'DELETE '.$this->quote($table).'.* FROM '
                .$this->quote($table).$joins.$where;
            return new MysqlExecutor($sql, $this->params);
    
        function compileBulkUpdate($queryset, array $what) {
            $model = $queryset->model;
    
            $table = $model::getMeta('table');
    
            $set = array();
            foreach ($what as $field=>$value)
    
                $set[] = sprintf('%s = %s', $this->quote($field), $this->input($value, false, $model));
    
            $set = implode(', ', $set);
    
            list($where, $having) = $this->getWhereHavingClause($queryset);
    
            $joins = $this->getJoins($queryset);
    
            $sql = 'UPDATE '.$this->quote($table).$joins.' SET '.$set.$where;
    
            return new MysqlExecutor($sql, $this->params);
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        // Returns meta data about the table used to build queries
        function inspectTable($table) {
    
            static $cache = array();
    
            // XXX: Assuming schema is not changing — add support to track
            //      current schema
            if (isset($cache[$table]))
                return $cache[$table];
    
            $sql = 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS '
    
                .'WHERE TABLE_NAME = '.db_input($table).' AND TABLE_SCHEMA = DATABASE() '
    
                .'ORDER BY ORDINAL_POSITION';
    
            $ex = new MysqlExecutor($sql, array());
    
            $columns = array();
            while (list($column) = $ex->getRow()) {
                $columns[] = $column;
            }
            return $cache[$table] = $columns;
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        var $stmt;
        var $fields = array();
        var $sql;
        var $params;
    
        // Array of [count, model] values representing which fields in the
        // result set go with witch model.  Useful for handling select_related
        // queries
        var $map;
    
        function __construct($sql, $params, $map=null) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            $this->sql = $sql;
            $this->params = $params;
    
            $this->map = $map;
        }
    
        function getMap() {
            return $this->map;
    
        function fixupParams() {
            $self = $this;
            $params = array();
    
            $sql = preg_replace_callback("/:(\d+)/",
    
            function($m) use ($self, &$params) {
    
                $params[] = $self->params[$m[1]-1];
                return '?';
            }, $this->sql);
            return array($sql, $params);
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function _prepare() {
    
            $this->execute();
            $this->_setup_output();
        }
    
        function execute() {
    
            list($sql, $params) = $this->fixupParams();
            if (!($this->stmt = db_prepare($sql)))
    
                throw new InconsistentModelException(
    
                    'Unable to prepare query: '.db_error().' '.$sql);
    
            if (count($params))
                $this->_bind($params);
    
            if (!$this->stmt->execute() || ! $this->stmt->store_result()) {
    
                throw new OrmException('Unable to execute query: ' . $this->stmt->error);
            }
            return true;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function _bind($params) {
            if (count($params) != $this->stmt->param_count)
    
                throw new Exception(__('Parameter count does not match query'));
    
    Jared Hancock's avatar
    Jared Hancock committed
    
            $types = '';
            $ps = array();
    
            foreach ($params as $i=>&$p) {
    
                if (is_int($p) || is_bool($p))
    
    Jared Hancock's avatar
    Jared Hancock committed
                    $types .= 'i';
    
                elseif (is_float($p))
                    $types .= 'd';
    
    Jared Hancock's avatar
    Jared Hancock committed
                elseif (is_string($p))
                    $types .= 's';
    
                elseif ($p instanceof DateTime) {
                    $types .= 's';
    
                    $p = $p->format('Y-m-d H:i:s');
    
                elseif (is_object($p)) {
                    $types .= 's';
                    $p = (string) $p;
                }
    
                // TODO: Emit error if param is null
    
    Jared Hancock's avatar
    Jared Hancock committed
                $ps[] = &$p;
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
            array_unshift($ps, $types);
            call_user_func_array(array($this->stmt,'bind_param'), $ps);
        }
    
        function _setup_output() {
    
            if (!($meta = $this->stmt->result_metadata()))
                throw new OrmException('Unable to fetch statment metadata: ', $this->stmt->error);
    
            $this->fields = $meta->fetch_fields();
    
            $meta->free_result();
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        // Iterator interface
        function rewind() {
            if (!isset($this->stmt))
                $this->_prepare();
            $this->stmt->data_seek(0);
        }
    
        function next() {
            $status = $this->stmt->fetch();
            if ($status === false)
    
                throw new OrmException($this->stmt->error);
    
    Jared Hancock's avatar
    Jared Hancock committed
            elseif ($status === null) {
                $this->close();
                return false;
            }
            return true;
        }
    
        function getArray() {
            $output = array();
            $variables = array();
    
            if (!isset($this->stmt))
                $this->_prepare();
    
            foreach ($this->fields as $f)
                $variables[] = &$output[$f->name]; // pass by reference
    
    
            if (!call_user_func_array(array($this->stmt, 'bind_result'), $variables))
                throw new OrmException('Unable to bind result: ' . $this->stmt->error);
    
    
    Jared Hancock's avatar
    Jared Hancock committed
            if (!$this->next())
                return false;
            return $output;
        }
    
    
        function getRow() {
            $output = array();
            $variables = array();
    
            if (!isset($this->stmt))
                $this->_prepare();
    
            foreach ($this->fields as $f)
                $variables[] = &$output[]; // pass by reference
    
    
            if (!call_user_func_array(array($this->stmt, 'bind_result'), $variables))
                throw new OrmException('Unable to bind result: ' . $this->stmt->error);
    
    
            if (!$this->next())
                return false;
            return $output;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function close() {
            if (!$this->stmt)
                return;
    
            $this->stmt->close();
            $this->stmt = null;
        }
    
    
        function affected_rows() {
            return $this->stmt->affected_rows;
        }
    
        function insert_id() {
            return $this->stmt->insert_id;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function __toString() {
    
            $self = $this;
    
            return preg_replace_callback("/:(\d+)(?=([^']*'[^']*')*[^']*$)/",
            function($m) use ($self) {
    
                $p = $self->params[$m[1]-1];
    
                if ($p instanceof DateTime) {
                    $p = $p->format('Y-m-d H:i:s');
                }
    
                return db_real_escape($p, is_string($p));
            }, $this->sql);
    
    class Q implements Serializable {
    
        const NEGATED = 0x0001;
        const ANY =     0x0002;
    
        var $constraints;
        var $flags;
        var $negated = false;
        var $ored = false;
    
    
        function __construct($filter=array(), $flags=0) {
    
            if (!is_array($filter))
                $filter = array($filter);
    
            $this->constraints = $filter;
            $this->negated = $flags & self::NEGATED;
            $this->ored = $flags & self::ANY;
        }
    
        function isNegated() {
            return $this->negated;
        }
    
        function isOred() {
            return $this->ored;
        }
    
        function negate() {
            $this->negated = !$this->negated;
            return $this;
        }
    
    
        function union() {
            $this->ored = true;
        }
    
        function add($constraints) {
            if (is_array($constraints))
                $this->constraints = array_merge($this->constraints, $constraints);
            elseif ($constraints instanceof static)
                $this->constraints[] = $constraints;
            else
                throw new InvalidArgumentException('Expected an instance of Q or an array thereof');
            return $this;
        }
    
    
        static function not($constraints) {
    
            return new static($constraints, self::NEGATED);
        }
    
    
        static function any($constraints) {
    
            return new static($constraints, self::ANY);
    
        static function all($constraints) {
            return new static($constraints);
        }
    
    
        function serialize() {
            return serialize(array(
                'f' =>
                    ($this->negated ? self::NEGATED : 0)
                  | ($this->ored ? self::ANY : 0),
                'c' => $this->constraints
            ));
        }
    
        function unserialize($data) {
            $data = unserialize($data);
            $this->constraints = $data['c'];
            $this->ored = $data['f'] & self::ANY;
            $this->negated = $data['f'] & self::NEGATED;
        }