Skip to content
Snippets Groups Projects
class.orm.php 119 KiB
Newer Older
Jared Hancock's avatar
Jared Hancock committed
        if ($queryset->related) {
            $fieldMap = $theseFields = array();
            $defer = $queryset->defer ?: array();
            // Add local fields first
            foreach ($meta->getFieldNames() 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()->getFieldNames() 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) {
            $additional_group_by = array();
            foreach ($queryset->values as $alias=>$v) {
                list($f) = $this->getField($v, $model);
                if ($f instanceof SqlFunction) {
                    $fields[$f->toSql($this, $model, $alias)] = true;
                    if ($f instanceof SqlAggregate) {
                        // Don't group_by aggregate expressions, but if there is an
                        // aggergate expression, then we need a GROUP BY clause.
                        $need_group_by = true;
                    if (!is_int($alias) && $unaliased != $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 && !$queryset->distinct)
                    $additional_group_by[] = $unaliased;
            if ($need_group_by && $additional_group_by)
                $group_by = array_merge($group_by, $additional_group_by);
        }
        // Simple selection from one table
        elseif (!$queryset->aggregated) {
                foreach ($meta->getFieldNames() 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
                if ($A instanceof SqlAggregate)
                    $need_group_by = true;
                $T = $A->toSql($this, $model, $alias);
                if ($fieldMap) {
                    array_splice($fields, count($fieldMap[0][0]), 0, array($T));
                    $fieldMap[0][0][] = $alias;
                }
                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 && !$queryset->distinct && $need_group_by) {
                foreach ($meta['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);
                else
                    $expr = sprintf('%s AS %s', $expr, $this->quote($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);
        if ($hint = $queryset->getOption(QuerySet::OPT_INDEX_HINT)) {
            $hint = " USE INDEX ({$hint})";
        }
        $sql = 'SELECT ';
        if ($queryset->hasOption(QuerySet::OPT_MYSQL_FOUND_ROWS))
            $sql .= 'SQL_CALC_FOUND_ROWS ';
        $sql .= implode(', ', $fields).' FROM '
            .$table.$hint.$joins.$where.$group_by.$having.$sort;
        // UNIONS
        if ($queryset->chain) {
            // If the main query is sorted, it will need parentheses
            if ($parens = (bool) $sort)
                $sql = "($sql)";
            foreach ($queryset->chain as $qs) {
                list($qs, $all) = $qs;
                $q = $qs->getQuery(array('nosort' => true));
                // Rewrite the parameter numbers so they fit the parameter numbers
                // of the current parameters of the $compiler
                $self = $this;
                $S = preg_replace_callback("/:(\d+)/",
                function($m) use ($self, $q) {
                    $self->params[] = $q->params[$m[1]-1];
                    return ':'.count($self->params);
                }, $q->sql);
                // Wrap unions in parentheses if they are windowed or sorted
                if ($parens || $qs->isWindowed() || count($qs->getSortFields()))
                    $S = "($S)";
                $sql .= ' UNION '.($all ? 'ALL ' : '').$S;
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, $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;
class MySqlPreparedExecutor {
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 setBuffered($buffered) {
        $this->unbuffered = !$buffered;
    }

    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->unbuffered || $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];
            switch (true) {
            case is_bool($p):
                $p = (int) $p;
            case is_int($p):
            case is_float($p):
                return $p;
            case $p instanceof DateTime:
                $p = $p->format('Y-m-d H:i:s');
Peter Rotich's avatar
Peter Rotich committed
                return db_real_escape((string) $p, true);
           }
        }, $this->sql);
/**
 * Simplified executor which uses the mysqli_query() function to process
 * queries. This method is faster on MySQL as it doesn't require the PREPARE
 * overhead, nor require two trips to the database per query. All parameters
 * are escaped and placed directly into the SQL statement. With this style,
 * it is possible that multiple parameters could compile a statement which
 * exceeds the MySQL max_allowed_packet setting.
 */
class MySqlExecutor
extends MySqlPreparedExecutor {
    function execute() {
        $sql = $this->__toString();
        if (!($this->stmt = db_query($sql, true, !$this->unbuffered)))
            throw new InconsistentModelException(
                'Unable to prepare query: '.db_error().' '.$sql);
        // mysqli_query() return TRUE for UPDATE queries and friends
        if ($this->stmt !== true)
            $this->_setupCast();
        return true;
    }

    function _setupCast() {
        $fields = $this->stmt->fetch_fields();
        $this->types = array();
        foreach ($fields as $F) {
            $this->types[] = $F->type;
        }
    }

    function _cast($record) {
        $i=0;
        foreach ($record as &$f) {
            switch ($this->types[$i++]) {
            case MYSQLI_TYPE_DECIMAL:
            case MYSQLI_TYPE_NEWDECIMAL:
            case MYSQLI_TYPE_LONGLONG:
            case MYSQLI_TYPE_FLOAT:
            case MYSQLI_TYPE_DOUBLE:
                $f = isset($f) ? (double) $f : $f;
                break;

            case MYSQLI_TYPE_BIT:
            case MYSQLI_TYPE_TINY:
            case MYSQLI_TYPE_SHORT:
            case MYSQLI_TYPE_LONG:
            case MYSQLI_TYPE_INT24:
                $f = isset($f) ? (int) $f : $f;
                break;

            default:
                // No change (leave as string)
            }
        }
        unset($f);
        return $record;
    }

    function getArray() {
        if (!isset($this->stmt))
            $this->execute();

        if (null === ($record = $this->stmt->fetch_assoc()))
            return false;
        return $this->_cast($record);
    }

    function getRow() {
        if (!isset($this->stmt))
            $this->execute();

        if (null === ($record = $this->stmt->fetch_row()))
            return false;
        return $this->_cast($record);
    }

    function affected_rows() {
        return db_affected_rows();
    }

    function insert_id() {
        return db_insert_id();
    }
}

class Q implements Serializable {
    const NEGATED = 0x0001;
    const ANY =     0x0002;

    var $constraints;
    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;
    }

    /**
     * Two neighboring Q's are compatible in a where clause if they have
     * the same boolean AND / OR operator. Negated Q's should always use
     * parentheses if there is more than one criterion.
     */
    function isCompatibleWith(Q $other) {
        return $this->ored == $other->ored;
    }

    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 evaluate($record) {
        // Start with FALSE for OR and TRUE for AND
        $result = !$this->ored;
        foreach ($this->constraints as $field=>$check) {
            $R = SqlCompiler::evaluate($record, $check, $field);
            if ($this->ored) {
                if ($result |= $R)
                    break;
            }
            elseif (!$R) {
                // Anything AND false
                $result = false;
                break;
            }
        }
        if ($this->negated)
            $result = !$result;
        return $result;
    }

    function serialize() {
        return serialize(array($this->negated, $this->ored, $this->constraints));
    }

    function unserialize($data) {
        list($this->negated, $this->ored, $this->constraints) = unserialize($data);