diff --git a/include/class.orm.php b/include/class.orm.php index ac81b8fd28f124342c1f80e1e3db01ecaa8bb544..a5b5ec0d51b7ab629570ac849509a42726182b5b 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -49,6 +49,8 @@ class ModelMeta implements ArrayAccess { } // Break down foreign-key metadata + if (!isset($meta['joins'])) + $meta['joins'] = array(); foreach ($meta['joins'] as $field => &$j) { if (isset($j['reverse'])) { list($model, $key) = explode('.', $j['reverse']); @@ -517,20 +519,53 @@ class ModelInstanceIterator implements Iterator, ArrayAccess { var $cache = array(); var $position = 0; var $queryset; + var $map; function __construct($queryset=false) { $this->queryset = $queryset; if ($queryset) { $this->model = $queryset->model; $this->resource = $queryset->getQuery(); + $this->map = $this->resource->getMap(); } } function buildModel($row) { // TODO: Traverse to foreign keys - $model = new $this->model($row); # nolint - $model->__deferred__ = $this->queryset->defer; - $model->__onload(); + if ($this->map) { + if ($this->model != $this->map[0][1]) + throw new OrmException('Internal select_related error'); + + $offset = 0; + foreach ($this->map as $info) { + @list($count, $model_class, $path, $alias) = $info; + $fields = array_slice($row, $offset, $count); + if (!$path) { + // Build the root model + $model = new $this->model($fields); # nolint + $model->__onload(); + } + else { + foreach ($fields as $name=>$val) { + $fields[substr($name, strlen($alias)+1)] = $val; + unset($fields[$name]); + } + // Link the related model + $tail = array_pop($path); + $m = $model; + foreach ($path as $field) { + $m = $m->get($field); + } + $m->set($tail, new $model_class($fields)); + } + $offset += $count; + } + } + else { + $model = new $this->model($row); # nolint + $model->__deferred__ = $this->queryset->defer; + $model->__onload(); + } return $model; } @@ -752,7 +787,9 @@ class SqlCompiler { $path = array(); $crumb = ''; - $alias = $this->quote($model::$meta['table']); + $alias = (isset($this->joins[''])) + ? $this->joins['']['alias'] + : $this->quote($model::$meta['table']); // Traverse through the parts and establish joins between the tables // if the field is joined to a foreign model @@ -779,6 +816,8 @@ class SqlCompiler { $field = $alias.'.'.$this->quote($field); else $field = $this->quote($field); + if (isset($options['model']) && $options['model']) + $operator = $model; return array($field, $operator); } @@ -1011,6 +1050,10 @@ class MySqlCompiler extends SqlCompiler { function compileSelect($queryset) { $model = $queryset->model; + // Use an alias for the root model table + $table = $model::$meta['table']; + $this->joins[''] = array('alias' => ($rootAlias = $this->nextAlias())); + // Compile the WHERE clause $where = $this->getWhereClause($queryset); $sort = ''; @@ -1028,23 +1071,56 @@ class MySqlCompiler extends SqlCompiler { $sort = ' ORDER BY '.implode(', ', $orders); } - // Include related tables + // Compile the field listing $fields = array(); - $table = $model::$meta['table']; + $table = $this->quote($table).' '.$rootAlias; + // Handle related tables if ($queryset->related) { - $fields = array($this->quote($table).'.*'); - foreach ($queryset->related as $rel) { - // XXX: This is ugly - list($t) = $this->getField($rel, $model, - array('table'=>true)); - $fields[] = $t.'.*'; + $count = 0; + $fieldMap = array(); + $defer = $queryset->defer ?: array(); + // Add local fields first + $model::_inspect(); + foreach ($model::$meta['fields'] as $f) { + // Handle deferreds + if (isset($defer[$f])) + continue; + $fields[] = $rootAlias . '.' . $this->quote($f); } - // Support only retrieving a list of values rather than a model - } elseif ($queryset->values) { + $count = count($fields); + $fieldMap[] = array($count, $model); + // Add the JOINs to this query + foreach ($queryset->related as $sr) { + $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)); + $fmodel::_inspect(); + foreach ($fmodel::$meta['fields'] as $f) { + // Handle deferreds + if (isset($defer[$sr . '__' . $f])) + continue; + $fields[] = $alias . '.' . $this->quote($f) . " AS {$alias}_$f"; + } + $fieldMap[] = array(count($fields) - $count, $fmodel, $parts, $alias); + $full_path .= '__'; + $count = count($fields); + } + } + } + // Support retrieving only a list of values rather than a model + elseif ($queryset->values) { foreach ($queryset->values as $v) { list($fields[]) = $this->getField($v, $model); } - } else { + } + // Simple selection from one table + else { if ($queryset->defer) { foreach ($model::$meta['fields'] as $f) { if (isset($queryset->defer[$f])) @@ -1053,13 +1129,13 @@ class MySqlCompiler extends SqlCompiler { } } else { - $fields[] = $this->quote($table).'.*'; + $fields[] = $rootAlias.'.*'; } } $joins = $this->getJoins(); $sql = 'SELECT '.implode(', ', $fields).' FROM ' - .$this->quote($table).$joins.$where.$sort; + .$table.$joins.$where.$sort; if ($queryset->limit) $sql .= ' LIMIT '.$queryset->limit; if ($queryset->offset) @@ -1073,7 +1149,7 @@ class MySqlCompiler extends SqlCompiler { break; } - return new MysqlExecutor($sql, $this->params); + return new MysqlExecutor($sql, $this->params, $fieldMap); } function __compileUpdateSet($model, array $pk) { @@ -1146,9 +1222,9 @@ class MySqlCompiler extends SqlCompiler { return $cache[$table]; $sql = 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS ' - .'WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE() ' + .'WHERE TABLE_NAME = '.db_input($table).' AND TABLE_SCHEMA = DATABASE() ' .'ORDER BY ORDINAL_POSITION'; - $ex = new MysqlExecutor($sql, array($table)); + $ex = new MysqlExecutor($sql, array()); $columns = array(); while (list($column) = $ex->getRow()) { $columns[] = $column; @@ -1161,13 +1237,21 @@ class MysqlExecutor { 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) { + function __construct($sql, $params, $map=null) { $this->sql = $sql; $this->params = $params; + $this->map = $map; + } + + function getMap() { + return $this->map; } function _prepare() { @@ -1211,8 +1295,7 @@ class MysqlExecutor { function _setup_output() { if (!($meta = $this->stmt->result_metadata())) throw new OrmException('Unable to fetch statment metadata: ', $this->stmt->error); - while ($f = $meta->fetch_field()) - $this->fields[] = $f; + $this->fields = $meta->fetch_fields(); $meta->free_result(); } @@ -1270,23 +1353,6 @@ class MysqlExecutor { return $output; } - function getStruct() { - $output = array(); - $variables = array(); - - if (!isset($this->stmt)) - $this->_prepare(); - - foreach ($this->fields as $f) - $variables[] = &$output[$f->table][$f->name]; // pass by reference - - // TODO: Figure out what the table alias for the root model will be - call_user_func_array(array($this->stmt, 'bind_result'), $variables); - if (!$this->next()) - return false; - return $output; - } - function close() { if (!$this->stmt) return;