Newer
Older
$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) {
$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)
// 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) {
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
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));
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;
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) {
if (is_int($p) || is_bool($p))
elseif (is_float($p))
$types .= 'd';
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
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();
}
// 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);
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);
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;
}
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;
}
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');
return db_real_escape((string) $p, true);
}
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
/**
* 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;
}
return serialize(array($this->negated, $this->ored, $this->constraints));
}
function unserialize($data) {
list($this->negated, $this->ored, $this->constraints) = unserialize($data);