Newer
Older
if ($options)
$this->options = array_merge($this->options, $options);
if ($options['subquery'])
$this->alias_num += 150;
}
function getParent() {
return $this->options['parent'];
/**
* Split a criteria item into the identifying pieces: path, field, and
* operator.
*/
static function splitCriteria($criteria) {
static $operators = array(
'exact' => 1, 'isnull' => 1,
'gt' => 1, 'lt' => 1, 'gte' => 1, 'lte' => 1, 'range' => 1,
'contains' => 1, 'like' => 1, 'startswith' => 1, 'endswith' => 1, 'regex' => 1,
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
'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
return $ops[$operator]($record->get($field), $check);
}
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
/**
* 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()) {
// 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.
list($field, $parts, $op) = static::splitCriteria($field);
$operator = static::$operators[$op];
$rootModel = $model;
// Call pushJoin for each segment in the join path. A new JOIN
// fragment will need to be emitted and/or cached
$joins = array();
$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'));
if (isset($options['table']) && $options['table'])
elseif (isset($this->annotations[$field]))
$field = $this->annotations[$field];
elseif ($alias)
$field = $alias.'.'.$this->quote($field);
if (isset($options['model']) && $options['model'])
$operator = $model;
/**
* 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);
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
/**
* 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) {
$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
$filter[] = call_user_func($op, $field, $value, $model);
$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);
}
}
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) {
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'),
'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);
// 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);
}
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));
}
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) {
// 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),
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;
$rmeta = $rmodel::getMeta();
$table = ($rmeta['view'])
// XXX: Support parameters from the nested query
? $rmodel::getSqlAddParams($this)
: $this->quote($rmeta['table']);
return array($base, $constraints);
/**
* input
*
* 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) {
elseif (!isset($what)) {
return 'NULL';
}
$this->params[] = $what;
return ':'.(count($this->params));
}
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) {
$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[] = "($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;
}
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
if ($columns = $queryset->getSortFields()) {
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);
if ($field instanceof SqlFunction)
$field = $field->toSql($this, $model);
// TODO: Throw exception if $field can be indentified as
// invalid
$orders[] = "{$field} {$dir}";
}
$sort = ' ORDER BY '.implode(', ', $orders);
}
// Compile the field listing
$fields = $group_by = array();
$meta = $model::getMeta();
$table = $this->quote($meta['table']).' '.$rootAlias;
$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;
$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;
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;
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);
$fields[$f->toSql($this, $model, $alias)] = true;
else {
if (!is_int($alias))
$f .= ' AS '.$this->quote($alias);
// If there are annotations, add in these fields to the
// GROUP BY clause
if ($queryset->annotations && !$queryset->distinct)
}
// Simple selection from one table
elseif (!$queryset->aggregated) {
if ($queryset->defer) {
foreach ($meta->getFieldNames() as $f) {
if (isset($queryset->defer[$f]))
continue;
$fields[$rootAlias .'.'. $this->quote($f)] = true;
}
}
else {
$fields[$rootAlias.'.*'] = true;
$fields = array_keys($fields);
// Add in annotations
if ($queryset->annotations) {
// 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));
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;
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));
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);
}
// 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 {
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;
var $unbuffered = false;
function __construct($sql, $params, $map=null) {
$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);
}
$this->execute();
$this->_setup_output();
}
function execute() {
list($sql, $params) = $this->fixupParams();
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;
}
function _bind($params) {
if (count($params) != $this->stmt->param_count)
throw new Exception(__('Parameter count does not match query'));
foreach ($params as $i=>&$p) {