diff --git a/include/class.export.php b/include/class.export.php index 9235a18bc93164c737c824ad59b01db4b0f9ee35..485628aa31009db73cc3bcca8217ae1c0aaaa569 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -60,6 +60,7 @@ class Export { $tickets = $sql->models() ->select_related('user', 'user__default_email', 'dept', 'staff', 'team', 'staff', 'cdata', 'topic', 'status', 'cdata__:priority') + ->options(QuerySet::OPT_NOCACHE) ->annotate(array( 'collab_count' => TicketThread::objects() ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1))) diff --git a/include/class.orm.php b/include/class.orm.php index e0120814d3a72170c4412c8fa59b7b067204778b..fc4c9b6c5e535f452f74e203ad0945fd135002a7 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -979,8 +979,16 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl const ASC = 'ASC'; const DESC = 'DESC'; + const OPT_NOSORT = 'nosort'; + const OPT_NOCACHE = 'nocache'; + + const ITER_MODELS = 1; + const ITER_HASH = 2; + const ITER_ROW = 3; + + var $iter = self::ITER_MODELS; + var $compiler = 'MySqlCompiler'; - var $iterator = 'ModelInstanceManager'; var $query; var $count; @@ -1103,7 +1111,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl } function models() { - $this->iterator = 'ModelInstanceManager'; + $this->iter = self::ITER_MODELS; $this->values = $this->related = array(); return $this; } @@ -1111,7 +1119,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl function values() { foreach (func_get_args() as $A) $this->values[$A] = $A; - $this->iterator = 'HashArrayIterator'; + $this->iter = self::ITER_HASH; // This disables related models $this->related = false; return $this; @@ -1119,7 +1127,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl function values_flat() { $this->values = func_get_args(); - $this->iterator = 'FlatArrayIterator'; + $this->iter = self::ITER_ROW; // This disables related models $this->related = false; return $this; @@ -1130,7 +1138,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl } function all() { - return $this->getIterator()->asArray(); + return $this->getIterator(); } function first() { @@ -1174,7 +1182,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl } $class = $this->compiler; $compiler = new $class(); - return $this->_count = $compiler->compileCount($this); + return $this->count = $compiler->compileCount($this); } function toSql($compiler, $model, $alias=false) { @@ -1241,10 +1249,18 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl } function options($options) { + // Make an array with $options as the only key + if (!is_array($options)) + $options = array($options => 1); + $this->options = array_merge($this->options, $options); return $this; } + function hasOption($option) { + return isset($this->options[$option]); + } + function countSelectFields() { $count = count($this->values) + count($this->annotations); if (isset($this->extra['select'])) @@ -1288,13 +1304,36 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl } // IteratorAggregate interface - function getIterator() { - $class = $this->iterator; - if (!isset($this->_iterator)) - $this->_iterator = new $class($this); + function getIterator($iterator=false) { + if (!isset($this->_iterator)) { + $class = $iterator ?: $this->getIteratorClass(); + $it = new $class($this); + if (!$this->hasOption(self::OPT_NOCACHE)) { + if ($this->iter == self::ITER_MODELS) + // Add findFirst() and such + $it = new ModelResultSet($it); + else + $it = new CachedResultSet($it); + } + else { + $it = $it->getIterator(); + } + $this->_iterator = $it; + } return $this->_iterator; } + function getIteratorClass() { + switch ($this->iter) { + case self::ITER_MODELS: + return 'ModelInstanceManager'; + case self::ITER_HASH: + return 'HashArrayIterator'; + case self::ITER_ROW: + return 'FlatArrayIterator'; + } + } + // ArrayAccess interface function offsetExists($offset) { return $this->getIterator()->offsetExists($offset); @@ -1391,78 +1430,124 @@ EOF; class DoesNotExist extends Exception {} class ObjectNotUnique extends Exception {} -abstract class ResultSet implements Iterator, ArrayAccess, Countable { - var $resource; - var $position = 0; - var $queryset; - var $cache = array(); +class CachedResultSet +implements IteratorAggregate, Countable, ArrayAccess { + protected $inner; + protected $eoi = false; + protected $cache = array(); - function __construct($queryset=false) { - $this->queryset = $queryset; - if ($queryset) { - $this->model = $queryset->model; - } + function __construct(IteratorAggregate $iterator) { + $this->inner = $iterator->getIterator(); } - function prime() { - if (!isset($this->resource) && $this->queryset) - $this->resource = $this->queryset->getQuery(); + function fillTo($level) { + while (!$this->eoi && count($this->cache) < $level) { + if (!$this->inner->valid()) { + $this->eoi = true; + break; + } + $this->cache[] = $this->inner->current(); + $this->inner->next(); + } } - abstract function fillTo($index); - function asArray() { $this->fillTo(PHP_INT_MAX); - return $this->cache; + return $this; } - // Iterator interface - function rewind() { - $this->position = 0; - } - function current() { - $this->fillTo($this->position); - return $this->cache[$this->position]; - } - function key() { - return $this->position; - } - function next() { - $this->position++; + function getCache() { + return $this->cache; } - function valid() { - $this->fillTo($this->position); - return count($this->cache) > $this->position; + + function getIterator() { + $this->asArray(); + return new ArrayIterator($this->cache); } - // ArrayAccess interface function offsetExists($offset) { - $this->fillTo($offset); - return $this->position >= $offset; + $this->fillTo($offset+1); + return count($this->cache) > $offset; } function offsetGet($offset) { - $this->fillTo($offset); + $this->fillTo($offset+1); return $this->cache[$offset]; } function offsetUnset($a) { - throw new Exception(sprintf(__('%s is read-only'), get_class($this))); + throw new Exception(__('QuerySet is read-only')); } function offsetSet($a, $b) { - throw new Exception(sprintf(__('%s is read-only'), get_class($this))); + throw new Exception(__('QuerySet is read-only')); } - // Countable interface function count() { - return count($this->asArray()); + $this->asArray(); + return count($this->cache); } } -class ModelInstanceManager extends ResultSet { +class ModelResultSet +extends CachedResultSet { + /** + * Find the first item in the current set which matches the given criteria. + * This would be used in favor of ::filter() which might trigger another + * database query. The criteria is intended to be quite simple and should + * not traverse relationships which have not already been fetched. + * Otherwise, the ::filter() or ::window() methods would provide better + * performance. + * + * Example: + * >>> $a = new User(); + * >>> $a->roles->add(Role::lookup(['name' => 'administator'])); + * >>> $a->roles->findFirst(['roles__name__startswith' => 'admin']); + * <Role: administrator> + */ + function findFirst($criteria) { + $records = $this->findAll($criteria, 1); + return @$records[0]; + } + + /** + * Find all the items in the current set which match the given criteria. + * This would be used in favor of ::filter() which might trigger another + * database query. The criteria is intended to be quite simple and should + * not traverse relationships which have not already been fetched. + * Otherwise, the ::filter() or ::window() methods would provide better + * performance, as they can provide results with one more trip to the + * database. + */ + function findAll($criteria, $limit=false) { + $records = array(); + foreach ($this as $record) { + $matches = true; + foreach ($criteria as $field=>$check) { + if (!SqlCompiler::evaluate($record, $field, $check)) { + $matches = false; + break; + } + } + if ($matches) + $records[] = $record; + if ($limit && count($records) == $limit) + break; + } + return $records; + } +} + +class ModelInstanceManager +implements IteratorAggregate { + var $queryset; var $model; var $map; static $objectCache = array(); + function __construct(QuerySet $queryset) { + $this->queryset = $queryset; + $this->model = $queryset->model; + } + function cache($model) { $key = sprintf('%s.%s', $model::$meta->model, implode('.', $model->get('pk'))); @@ -1564,7 +1649,7 @@ class ModelInstanceManager extends ResultSet { * describes the relationship between the root model and this model, * 'user__account' for instance. */ - function buildModel($row) { + function buildModel($row, $cache=true) { // TODO: Traverse to foreign keys if ($this->map) { if ($this->model != $this->map[0][1]) @@ -1577,7 +1662,7 @@ class ModelInstanceManager extends ResultSet { $record = array_combine($fields, $values); if (!$path) { // Build the root model - $model = $this->getOrBuild($this->model, $record); + $model = $this->getOrBuild($this->model, $record, $cache); } elseif ($model) { $i = 0; @@ -1588,71 +1673,137 @@ class ModelInstanceManager extends ResultSet { if (!($m = $m->get($field))) break; } - if ($m) - $m->set($tail, $this->getOrBuild($model_class, $record)); + if ($m) { + // Only apply cache setting to the root model. + // Reference models should use caching + $m->set($tail, $this->getOrBuild($model_class, $record, $cache)); + } } $offset += count($fields); } } else { - $model = $this->getOrBuild($this->model, $row); + $model = $this->getOrBuild($this->model, $row, $cache); } return $model; } - function fillTo($index) { - $this->prime(); + function getIterator() { + $this->resource = $this->queryset->getQuery(); + $this->map = $this->resource->getMap(); + $cache = !$this->queryset->hasOption(QuerySet::OPT_NOCACHE); + $this->resource->setBuffered($cache); $func = ($this->map) ? 'getRow' : 'getArray'; - while ($this->resource && $index >= count($this->cache)) { - if ($row = $this->resource->{$func}()) { - $this->cache[] = $this->buildModel($row); - } else { - $this->resource->close(); - $this->resource = false; - break; - } - } + $func = array($this->resource, $func); + + return new CallbackSimpleIterator(function() use ($func, $cache) { + global $StopIteration; + + if ($row = $func()) + return $this->buildModel($row, $cache); + + $this->resource->close(); + throw $StopIteration; + }); + } +} + +class CallbackSimpleIterator +implements Iterator { + var $current; + var $eoi; + var $callback; + var $key = -1; + + function __construct($callback) { + assert(is_callable($callback)); + $this->callback = $callback; } - function prime() { - parent::prime(); - if ($this->resource) { - $this->map = $this->resource->getMap(); + function rewind() { + $this->eoi = false; + $this->next(); + } + + function key() { + return $this->key; + } + + function valid() { + if (!isset($this->eoi)) + $this->rewind(); + return !$this->eoi; + } + + function current() { + if ($this->eoi) return false; + return $this->current; + } + + function next() { + try { + $cbk = $this->callback; + $this->current = $cbk(); + $this->key++; + } + catch (StopIteration $x) { + $this->eoi = true; } } } -class FlatArrayIterator extends ResultSet { - function fillTo($index) { - $this->prime(); - while ($this->resource && $index >= count($this->cache)) { - if ($row = $this->resource->getRow()) { - $this->cache[] = $row; - } else { - $this->resource->close(); - $this->resource = false; - break; - } - } +// Use a global variable, as constructing exceptions is expensive +class StopIteration extends Exception {} +$StopIteration = new StopIteration(); + +class FlatArrayIterator +implements IteratorAggregate { + var $queryset; + var $resource; + + function __construct(QuerySet $queryset) { + $this->queryset = $queryset; + } + + function getIterator() { + $this->resource = $this->queryset->getQuery(); + return new CallbackSimpleIterator(function() { + global $StopIteration; + + if ($row = $this->resource->getRow()) + return $row; + + $this->resource->close(); + throw $StopIteration; + }); } } -class HashArrayIterator extends ResultSet { - function fillTo($index) { - $this->prime(); - while ($this->resource && $index >= count($this->cache)) { - if ($row = $this->resource->getArray()) { - $this->cache[] = $row; - } else { - $this->resource->close(); - $this->resource = false; - break; - } - } +class HashArrayIterator +implements IteratorAggregate { + var $queryset; + var $resource; + + function __construct(QuerySet $queryset) { + $this->queryset = $queryset; + } + + function getIterator() { + $this->resource = $this->queryset->getQuery(); + return new CallbackSimpleIterator(function() { + global $StopIteration; + + if ($row = $this->resource->getArray()) + return $row; + + $this->resource->close(); + throw $StopIteration; + }); } } -class InstrumentedList extends ModelInstanceManager { +class InstrumentedList +extends ModelResultSet { var $key; function __construct($fkey, $queryset=false) { @@ -1662,8 +1813,9 @@ class InstrumentedList extends ModelInstanceManager { if ($related = $model::getMeta('select_related')) $queryset->select_related($related); } - parent::__construct($queryset); + parent::__construct(new ModelInstanceManager($queryset)); $this->model = $model; + $this->queryset = $queryset; } function add($object, $at=false) { @@ -1728,34 +1880,6 @@ class InstrumentedList extends ModelInstanceManager { return new static(array($this->model, $key), $this->filter($constraint)); } - /** - * Find the first item in the current set which matches the given criteria. - * This would be used in favor of ::filter() which might trigger another - * database query. The criteria is intended to be quite simple and should - * not traverse relationships which have not already been fetched. - * Otherwise, the ::filter() or ::window() methods would provide better - * performance. - * - * Example: - * >>> $a = new User(); - * >>> $a->roles->add(Role::lookup(['name' => 'administator'])); - * >>> $a->roles->findFirst(['roles__name__startswith' => 'admin']); - * <Role: administrator> - */ - function findFirst(array $criteria) { - foreach ($this as $record) { - $matches = true; - foreach ($criteria as $field=>$check) { - if (!SqlCompiler::evaluate($record, $field, $check)) { - $matches = false; - break; - } - } - if ($matches) - return $record; - } - } - /** * Sort the instrumented list in place. This would be useful to change the * sorting order of the items in the list without fetching the list from @@ -2784,6 +2908,9 @@ class MySqlExecutor { // queries var $map; + var $conn; + var $unbuffered = false; + function __construct($sql, $params, $map=null) { $this->sql = $sql; $this->params = $params; @@ -2794,6 +2921,14 @@ class MySqlExecutor { return $this->map; } + function setBuffered($buffered) { + $this->unbuffered = !$buffered; + if (!$buffered) { + // Execute this query in another session + $this->conn = Bootstrap::connect(); + } + } + function fixupParams() { $self = $this; $params = array(); @@ -2812,12 +2947,12 @@ class MySqlExecutor { function execute() { list($sql, $params) = $this->fixupParams(); - if (!($this->stmt = db_prepare($sql))) + if (!($this->stmt = db_prepare($sql, $this->conn))) throw new InconsistentModelException( - 'Unable to prepare query: '.db_error().' '.$sql); + 'Unable to prepare query: '.db_error($this->conn).' '.$sql); if (count($params)) $this->_bind($params); - if (!$this->stmt->execute() || ! $this->stmt->store_result()) { + if (!$this->stmt->execute() || !($this->unbuffered || $this->stmt->store_result())) { throw new OrmException('Unable to execute query: ' . $this->stmt->error); } return true; diff --git a/include/mysqli.php b/include/mysqli.php index 2a79feaa745bd966f596e1830c0151fa2aa7dbb4..4ab5e8cf39300d757d3e247f22a8092d795248b8 100644 --- a/include/mysqli.php +++ b/include/mysqli.php @@ -69,9 +69,12 @@ function db_connect($host, $user, $passwd, $options = array()) { if(isset($options['db'])) $__db->select_db($options['db']); //set desired encoding just in case mysql charset is not UTF-8 - Thanks to FreshMedia - @$__db->query('SET NAMES "utf8"'); - @$__db->query('SET CHARACTER SET "utf8"'); - @$__db->query('SET COLLATION_CONNECTION=utf8_general_ci'); + @db_set_all(array( + 'NAMES' => 'utf8', + 'CHARACTER SET' => 'utf8', + 'COLLATION_CONNECTION' => 'utf8_general_ci', + 'SQL_MODE' => '', + ), 'session'); $__db->set_charset('utf8'); @db_set_variable('sql_mode', ''); @@ -123,10 +126,30 @@ function db_get_variable($variable, $type='session') { } function db_set_variable($variable, $value, $type='session') { - $sql =sprintf('SET %s %s=%s',strtoupper($type), $variable, db_input($value)); - return db_query($sql); + return db_set_all(array($variable => $value), $type); } +function db_set_all($variables, $type='session') { + global $__db; + + $set = array(); + $type = strtoupper($type); + foreach ($variables as $k=>$v) { + $k = strtoupper($k); + $T = $type; + if (in_array($k, ['NAMES', 'CHARACTER SET'])) { + // MySQL doesn't support the session/global flag, and doesn't + // use an equal sign for these + $type = ''; + } + else { + $k .= ' = '; + } + $set[] = "$type $k ".($__db->real_escape_string($v) ?: "''"); + } + $sql = 'SET ' . implode(', ', $set); + return db_query($sql); +} function db_select_database($database) { global $__db; @@ -194,17 +217,6 @@ function db_query_unbuffered($sql, $logError=false) { return db_query($sql, $logError, true); } -function db_squery($query) { //smart db query...utilizing args and sprintf - - $args = func_get_args(); - $query = array_shift($args); - $query = str_replace("?", "%s", $query); - $args = array_map('db_real_escape', $args); - array_unshift($args, $query); - $query = call_user_func_array('sprintf', $args); - return db_query($query); -} - function db_count($query) { return db_result(db_query($query)); }