Skip to content
Snippets Groups Projects
class.orm.php 107 KiB
Newer Older
    function __construct($options=false) {
        if ($options)
            $this->options = array_merge($this->options, $options);
        if ($options['subquery'])
            $this->alias_num += 150;
    }

    function getParent() {
        return $this->options['parent'];
Jared Hancock's avatar
Jared Hancock committed
    /**
     * Split a criteria item into the identifying pieces: path, field, and
     * operator.
     */
    static function splitCriteria($criteria) {
        static $operators = array(
            'exact' => 1, 'isnull' => 1,
Jared Hancock's avatar
Jared Hancock committed
            'gt' => 1, 'lt' => 1, 'gte' => 1, 'lte' => 1, 'range' => 1,
            'contains' => 1, 'like' => 1, 'startswith' => 1, 'endswith' => 1, 'regex' => 1,
Jared Hancock's avatar
Jared Hancock committed
            'in' => 1, 'intersect' => 1,
            'hasbit' => 1,
        );
        $path = explode('__', $criteria);
        if (!isset($options['table'])) {
            $field = array_pop($path);
            if (isset($operators[$field])) {
                $operator = $field;
                $field = array_pop($path);
            }
        }
        return array($field, $path, $operator ?: 'exact');
    }

    /**
     * Check if the values match given the operator.
     *
     * Throws:
     * OrmException - if $operator is not supported
     */
    static function evaluate($record, $field, $check) {
        static $ops; if (!isset($ops)) { $ops = array(
            'exact' => function($a, $b) { return is_string($a) ? strcasecmp($a, $b) == 0 : $a == $b; },
            'isnull' => function($a, $b) { return is_null($a) == $b; },
            'gt' => function($a, $b) { return $a > $b; },
            'gte' => function($a, $b) { return $a >= $b; },
            'lt' => function($a, $b) { return $a < $b; },
            'lte' => function($a, $b) { return $a <= $b; },
            'contains' => function($a, $b) { return stripos($a, $b) !== false; },
            'startswith' => function($a, $b) { return stripos($a, $b) === 0; },
            'hasbit' => function($a, $b) { return $a & $b == $b; },
        ); }
        list($field, $path, $operator) = self::splitCriteria($field);
        if (!isset($ops[$operator]))
            throw new OrmException($operator.': Unsupported operator');

        if ($path)
            $record = $record->getByPath($path);
        // TODO: Support Q expressions
Jared Hancock's avatar
Jared Hancock committed
        return $ops[$operator]($record->get($field), $check);
    }

    /**
     * Handles breaking down a field or model search descriptor into the
     * model search path, field, and operator parts. When used in a queryset
     * filter, an expression such as
     *
     * user__email__hostname__contains => 'foobar'
     *
     * would be broken down to search from the root model (passed in,
     * perhaps a ticket) to the user and email models by inspecting the
     * model metadata 'joins' property. The 'constraint' value found there
     * will be used to build the JOIN sql clauses.
     *
     * The 'hostname' will be the field on 'email' model that should be
     * compared in the WHERE clause. The comparison should be made using a
     * 'contains' function, which in MySQL, might be implemented using
     * something like "<lhs> LIKE '%foobar%'"
     *
     * This function will rely heavily on the pushJoin() function which will
     * handle keeping track of joins made previously in the query and
     * therefore prevent multiple joins to the same table for the same
     * reason. (Self joins are still supported).
     *
     * Comparison functions supported by this function are defined for each
     * respective SqlCompiler subclass; however at least these functions
     * should be defined:
     *
     *      function    a__function => b
     *      ----------+------------------------------------------------
     *      exact     | a is exactly equal to b
     *      gt        | a is greater than b
     *      lte       | b is greater than a
     *      lt        | a is less than b
     *      gte       | b is less than a
     *      ----------+------------------------------------------------
     *      contains  | (string) b is contained within a
     *      statswith | (string) first len(b) chars of a are exactly b
     *      endswith  | (string) last len(b) chars of a are exactly b
     *      like      | (string) a matches pattern b
     *      ----------+------------------------------------------------
     *      in        | a is in the list or the nested queryset b
     *      ----------+------------------------------------------------
     *      isnull    | a is null (if b) else a is not null
     *
     * If no comparison function is declared in the field descriptor,
     * 'exact' is assumed.
     *
     * Parameters:
     * $field - (string) name of the field to join
     * $model - (VerySimpleModel) root model for references in the $field
     *      parameter
     * $options - (array) extra options for the compiler
     *      'table' => return the table alias rather than the field-name
     *      'model' => return the target model class rather than the operator
     *      'constraint' => extra constraint for join clause
     *
     * Returns:
     * (mixed) Usually array<field-name, operator> where field-name is the
     * name of the field in the destination model, and operator is the
     * requestion comparison method.
     */
    function getField($field, $model, $options=array()) {
Jared Hancock's avatar
Jared Hancock committed
        // Break apart the field descriptor by __ (double-underbars). The
        // first part is assumed to be the root field in the given model.
        // The parts after each of the __ pieces are links to other tables.
        // The last item (after the last __) is allowed to be an operator
        // specifiction.
Jared Hancock's avatar
Jared Hancock committed
        list($field, $parts, $op) = static::splitCriteria($field);
        $operator = static::$operators[$op];
        // Call pushJoin for each segment in the join path. A new JOIN
        // fragment will need to be emitted and/or cached
        $null = false;
        $push = function($p, $model) use (&$joins, &$path, &$null) {
            $J = $model::getMeta('joins');
            if (!($info = $J[$p])) {
                throw new OrmException(sprintf(
                   'Model `%s` does not have a relation called `%s`',
                    $model, $p));
            }
            // Propogate LEFT joins through other joins. That is, if a
            // multi-join expression is used, the first LEFT join should
            // result in further joins also being LEFT
            if (isset($info['null']))
                $null = $null || $info['null'];
            $info['null'] = $null;
            $crumb = $path;
            $path = ($path) ? "{$path}__{$p}" : $p;
            $joins[] = array($crumb, $path, $model, $info);
            // Roll to foreign model
            return $info['fkey'];
        foreach ($parts as $p) {
            list($model) = $push($p, $model);
        }

        // If comparing a relationship, join the foreign table
        // This is a comparison with a relationship — use the foreign key
        $J = $model::getMeta('joins');
        if (isset($J[$field])) {
            list($model, $field) = $push($field, $model);
        }

        // Apply the joins list to $this->pushJoin
        $last = count($joins) - 1;
        $constraint = false;
        foreach ($joins as $i=>$A) {
            // Add the conststraint as the last arg to the last join
            if ($i == $last)
                $constraint = $options['constraint'];
            $alias = $this->pushJoin($A[0], $A[1], $A[2], $A[3], $constraint);
        }

        if (!isset($alias)) {
            // Determine the alias for the root model table
            $alias = (isset($this->joins['']))
                ? $this->joins['']['alias']
                : $this->quote($rootModel::getMeta('table'));
Jared Hancock's avatar
Jared Hancock committed
        if (isset($options['table']) && $options['table'])
            $field = $alias;
        elseif (isset($this->annotations[$field]))
            $field = $this->annotations[$field];
        elseif ($alias)
            $field = $alias.'.'.$this->quote($field);
Jared Hancock's avatar
Jared Hancock committed
        else
            $field = $this->quote($field);
        if (isset($options['model']) && $options['model'])
            $operator = $model;
        return array($field, $operator);
    /**
     * Uses the compiler-specific `compileJoin` function to compile the join
     * statement fragment, and caches the result in the local $joins list. A
     * new alias is acquired using the `nextAlias` function which will be
     * associated with the join. If the same path is requested again, the
     * algorithm is short-circuited and the originally-assigned table alias
     * is returned immediately.
     */
    function pushJoin($tip, $path, $model, $info, $constraint=false) {
        // TODO: Build the join statement fragment and return the table
        // alias. The table alias will be useful where the join is used in
        // the WHERE and ORDER BY clauses

        // If the join already exists for the statement-being-compiled, just
        // return the alias being used.
        if (!$constraint && isset($this->joins[$path]))
            return $this->joins[$path]['alias'];

        // TODO: Support only using aliases if necessary. Use actual table
        // names for everything except oddities like self-joins

        $alias = $this->nextAlias();
        // Keep an association between the table alias and the model. This
        // will make model construction much easier when we have the data
        // and the table alias from the database.
        $this->aliases[$alias] = $model;

        // TODO: Stash joins and join constraints into local ->joins array.
        // This will be useful metadata in the executor to construct the
        // final models for fetching
        // TODO: Always use a table alias. This will further help with
        // coordination between the data returned from the database (where
        // table alias is available) and the corresponding data.
        $T = array('alias' => $alias);
        $this->joins[$path] = $T;
        $this->joins[$path]['sql'] = $this->compileJoin($tip, $model, $alias, $info, $constraint);
    /**
     * compileQ
     *
     * Build a constraint represented in an arbitrarily nested Q instance.
     * The placement of the compiled constraint is also considered and
     * represented in the resulting CompiledExpression instance.
     *
     * Parameters:
     * $Q - (Q) constraint represented in a Q instance
     * $model - (VerySimpleModel) root model for all the field references in
     *      the Q instance
     * $slot - (int) slot for inputs to be placed. Useful to differenciate
     *      inputs placed in the joins and where clauses for SQL engines
     *      which do not support named parameters.
     *
     * Returns:
     * (CompiledExpression) object containing the compiled expression (with
     * AND, OR, and NOT operators added). Furthermore, the $type attribute
     * of the CompiledExpression will allow the compiler to place the
     * constraint properly in the WHERE or HAVING clause appropriately.
     */
    function compileQ(Q $Q, $model, $slot=false) {
        $filter = array();
        $type = CompiledExpression::TYPE_WHERE;
        foreach ($Q->constraints as $field=>$value) {
            // Handle nested constraints
            if ($value instanceof Q) {
                $filter[] = $T = $this->compileQ($value, $model, $slot);
                // Bubble up HAVING constraints
                if ($T instanceof CompiledExpression
                        && $T->type == CompiledExpression::TYPE_HAVING)
                    $type = $T->type;
            // Handle relationship comparisons with model objects
            elseif ($value instanceof VerySimpleModel) {
                $criteria = array();
                // Avoid a join if possible. Use the local side of the
                // relationship
                if (count($value->pk) === 1) {
                    $path = explode('__', $field);
                    $relationship = array_pop($path);
                    $lmodel = $model::getMeta()->getByPath($path);
                    $local = $lmodel['joins'][$relationship]['local'];
                    $path = $path ? (implode('__', $path) . '__') : '';
                    foreach ($value->pk as $v) {
                        $criteria["{$path}{$local}"] = $v;
                   }
                }
                else {
                    foreach ($value->pk as $f=>$v) {
                        $criteria["{$field}__{$f}"] = $v;
                    }
                }
                $filter[] = $this->compileQ(new Q($criteria), $model, $slot);
            }
            // Handle simple field = <value> constraints
                list($field, $op) = $this->getField($field, $model);
                if ($field instanceof SqlAggregate) {
                    // This constraint has to go in the HAVING clause
                    $field = $field->toSql($this, $model);
                    $type = CompiledExpression::TYPE_HAVING;
                }
                if ($value === null)
                    $filter[] = sprintf('%s IS NULL', $field);
                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));
        $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',
Jared Hancock's avatar
Jared Hancock committed
        'range' => array('self', '__range'),
        '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'),
        'regex' => array('self', '__regex'),
    // 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 is almost always faster with a join. Use one if possible
        // 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() || $b->countSelectFields() > 1 || $b->chain)
        ) {
            $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 __regex($a, $b) {
        // Strip slashes and options
        if ($b[0] == '/')
            $b = preg_replace('`/[^/]*$`', '', substr($b, 1));
        return sprintf('%s REGEXP %s', $a, $this->input($b));
    }

Jared Hancock's avatar
Jared Hancock committed
    function __range($a, $b) {
        // XXX: Crash if $b is not array of two items
        return sprintf('%s BETWEEN %s AND %s', $a, $b[0], $b[1]);
    }

    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
        $rmeta = $rmodel::getMeta();
        $table = ($rmeta['view'])
            // XXX: Support parameters from the nested query
            ? $rmodel::getSqlAddParams($this)
            : $this->quote($rmeta['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, $model=false) {
        if ($what instanceof QuerySet) {
            $q = $what->getQuery(array('nosort'=>!($what->limit || $what->offset)));
            // 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) {
Peter Rotich's avatar
Peter Rotich committed
        return sprintf("`%s`", str_replace("`", "``", $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) {
        $q = clone $queryset;
        // Drop extra fields from the queryset
        $q->related = $q->anotations = false;
        $model = $q->model;
        $q->values = $model::getMeta('pk');
        $exec = $q->getQuery(array('nosort' => true));
        $exec->sql = 'SELECT COUNT(*) FROM ('.$exec->sql.') __';
        $row = $exec->getRow();
        return is_array($row) ? (int) $row[0] : null;
Jared Hancock's avatar
Jared Hancock committed
    }

    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 (is_array($sort)) {
                    list($sort, $dir) = $sort;
                }
                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();
        $meta = $model::getMeta();
        $table = $this->quote($meta['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 ($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) {
            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 && !$queryset->distinct)
                    $group_by[] = $unaliased;
        }
        // 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
                $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) {
                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);
        $sql = 'SELECT '.implode(', ', $fields).' FROM '
            .$table.$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();