=== modified file 'includes/entity.inc' --- includes/entity.inc 2010-03-27 05:52:49 +0000 +++ includes/entity.inc 2010-05-03 09:11:30 +0000 @@ -298,3 +298,339 @@ class DrupalDefaultEntityController impl $this->entityCache += $entities; } } + + +/** + * Exception thrown by EntityFieldQuery() on unsupported query syntax. + * + * Some storage modules might not support the full range of the syntax for + * conditions, and will raise a EntityFieldQueryException when an usupported + * condition was specified. + */ +class EntityFieldQueryException extends FieldException {} + +/** + * Retrieve entities matching a given set of conditions. + * + * Storage engines are not required to support all kinds of queries. A + * EntityFieldQueryException will be raised if an unsupported condition is specified + * or if the query has field conditions / sorts which are stored in different + * field storage engines. It entirely depends on the field storage engine what + * sort of queries it supports and what kind it does not. For example, if a + * field is stored in a separate datastore than the entities they belong to + * then it's very likely that passing in both a fieldCondition and an + * entityCondition will raise this exception. + * + * The class methods itself just collection information passed to the methods, + * there is no logic before hook_entity_query() is fired in the execute method + * so this hook can change EntityFieldQuery totally. If this hook does not + * provide a result, then EntityFieldQuery::execute() finds the field storage + * engine and hands over the query to hook_field_storage_query. If there are + * neither field conditions nor field orders then + * EntityFieldQuery::entityQuery() queries the SQL entity tables. + */ +class EntityFieldQuery { + /** + * List of field conditions. + * + * @see EntityFieldQuery::fieldCondition(). + */ + protected $fieldConditions = array(); + + /** + * List of entity conditions. + * + * @see EntityFieldQuery::entityCondition(). + */ + protected $entityConditions = array(); + + /** + * List of entity types. + * + * @see EntityFieldQuery::entityTypes(). + */ + protected $entityTypes = array(); + + /** + * List of orders on fields. + * + * @see EntityFieldQuery::fieldOrderBy(). + */ + protected $fieldOrder = array(); + + /** + * List of orders on entities. + * + * @see EntityFieldQuery::entityOrderBy(). + */ + protected $entityOrder = array(); + + /** + * The query range. + * + * @see EntityFieldQuery::range(). + */ + protected $range = array(); + + /** + * Whether to query the deleted column and it's value. + * + * @see EntityFieldQuery::deleted(). + */ + protected $deleted = FALSE; + + /** + * The field names used. + */ + protected $fieldNames = array(); + + /** + * A condition on field values. + * + * @param $field_name + * Name of the field. + * @param $column + * A column defined in the hook_field_schema() of this field. entity_id and + * bundle can also be used. + * @param $value + * The value to test the column value against. In most cases, this is a + * scalar. For more complex options, it is an array. The meaning of each + * element in the array is dependent on the $operator. + * @param $operator + * Possible values: + * - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH': these operators expect + * the value as a literal of the same type as the column, + * - 'IN', 'NOT IN': this operator expects the value as an array of + * literals of the same type as the column. + * - 'BETWEEN': this operator expects the value as an array of two literals + * of the same type as the column. + * The operator can be ommitted, and will default to 'IN' if the value is an + * array, or to '=' otherwise. + * @param $delta_group + * An arbitrary identifier, conditions in the same group must have the same + * delta. For example, if a multivalue field has two columns, 'color' and + * 'shape' and two values are red square and blue circle and the query + * should find a red circle then the two conditions + * (color = red, shape = circle) will wrongly find this entity. If they + * have the same delta then it will work as expected. + * @param $language_group + * An arbitrary identifier, conditions in the same group must have the same + * language. + * @return EntityFieldQuery + * The called object. + */ + public function fieldCondition($field_name, $column, $value, $operator = NULL, $delta_group = NULL, $language_group = NULL) { + $this->fieldConditions[] = array( + 'field_name' => $field_name, + 'column' => $column, + 'value' => $value, + 'operator' => $operator, + 'delta_group' => $delta_group, + 'language_group' => $language_group, + ); + $this->fieldNames[] = $field_name; + } + + /** + * A condition on the entity base table. + * + * entityTypes() and entityCondition() can not be used on the same query as + * entityCondition() also indicates the single entity_type to be used. + * + * @param $entity_type + * An entity type. + * @param $column + * A column defined in the hook_schema() of the base table of this entity + * type. + * @param $value + * The value to test the field against. In most cases, this is a scalar. For more + * complex options, it is an array. The meaning of each element in the array is + * dependent on the $operator. + * @param $operator + * Possible values: + * - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH': these operators expect + * the value as a literal of the same type as the column, + * - 'IN', 'NOT IN': this operator expects the value as an array of + * literals of the same type as the column. + * - 'BETWEEN': this operator expects the value as an array of two literals + * of the same type as the column. + * The operator can be ommitted, and will default to 'IN' if the value is an + * array, or to '=' otherwise. + * @return EntityFieldQuery + * The called object. + */ + public function entityCondition($entity_type, $column, $value, $operator = NULL) { + $this->entityConditions[] = array( + 'entity_type' => $entity_type, + 'column' => $column, + 'value' => $value, + 'operator' => $operator, + ); + } + + /** + * A list of entity types to restrict the query to. + * + * entityTypes() and entityCondition() can not be used on the same query as + * entityCondition() also indicates the single entity_type to be used. + * + * @param $entity_types + * A list of entity types. + */ + public function entityTypes($entity_types) { + $this->entityTypes = $entity_types; + } + + /** + * Orders the result set by a given entity column. + * + * If called multiple times, the query will order by each specified column in + * the order this method is called. + * + * @param $column + * The column on which to order. + * @param $direction + * The direction to sort. Legal values are "ASC" and "DESC". + * @return EntityFieldQuery + * The called object. + */ + public function entityOrderBy($column, $direction) { + $this->entityOrder[] = array( + 'column' => $column, + 'direction' => $direction, + ); + } + + /** + * Orders the result set by a given field column. + * + * If called multiple times, the query will order by each specified column in + * the order this method is called. + * + * @param $field_name + * Name of the field. + * @param $column + * A column defined in the hook_field_schema() of this field. entity_id and + * bundle can also be used. + * @param $direction + * The direction to sort. Legal values are "ASC" and "DESC". + * @return EntityFieldQuery + * The called object. + */ + public function fieldOrderBy($field_name, $column, $direction) { + $this->fieldOrder[] = array( + 'field_name' => $field_name, + 'column' => $column, + 'direction' => $direction, + ); + $this->fieldNames[] = $field_name; + } + + /** + * Restricts a query to a given range in the result set. + * + * @param $start + * The first entity from the result set to return. If NULL, removes any + * range directives that are set. + * @param $limit + * The number of entities to return from the result set. + * @return EntityFieldQuery + * The called object. + */ + public function range($start = NULL, $length = NULL) { + $this->range = array( + 'start' => $start, + 'length' => $length, + ); + } + + /** + * Filter on the data being deleted. + * + * @param $deleted + * TRUE to only return deleted data, FALSE to return non-deleted data, NULL + * to return everything. Defaults to FALSE. + * @return EntityFieldQuery + * The called object. + */ + public function deleted($deleted) { + $this->deleted = $deleted; + } + + + /** + * Execute the query. + * + * @return + * An associative array, the keys are entity_types, the values are entities + * as returned by entity_load() for that entity_type. + * @code + * foreach ($query->execute() as $entity_type => $entities) { + * foreach ($entities as $entity_id => $entity) { + * @endcode + */ + public function execute() { + foreach (module_implements('entity_query') as $module) { + $function = $module . '_entity_query'; + $result = $function($this->entityTypes, $this->entityConditions, $this->fieldConditions, $this->fieldOrder, $this->entityOrder, $this->range, $this->deleted); + if (isset($result)) { + return $result; + } + } + foreach ($this->fieldNames as $field_name) { + $field = field_info_field($field_name); + if (!isset($storage)) { + $storage = $field['storage']; + } + elseif (array_diff($storage, $field['storage'])) { + throw new EntityFieldQueryException(t("Can't handle more than one field storage engine")); + } + } + if ($this->fieldNames) { + $function = $storage['module'] . '_field_storage_query'; + return $function($this->entityTypes, $this->entityConditions, $this->fieldConditions, $this->fieldOrder, $this->entityOrder, $this->range, $this->deleted); + } + return $this->entityQuery(); + } + + protected function entityQuery() { + $entity_type = ''; + if ($this->entityConditions) { + $entity_type = $this->entityConditions[0]['entity_type']; + } + else { + $n = count($this->entityTypes); + if ($n == 1) { + $entity_type = $this->entityTypes[0]; + } + elseif ($n > 1) { + throw new EntityFieldQueryException(t("Can't query multiple entity tables at once.")); + } + else { + throw new EntityFieldQueryException(t('Empty query')); + } + } + $entity_info = entity_get_info($entity_type); + $base_table = isset($entity_info['base_table']) ? $entity_info['base_table'] : $entity_type; + $query = db_select($base_table); + $query->addField($base_table, $entity_info['entity keys']['id'], 'entity_id'); + $query->addExpression(':entity_type', 'entity_type', array(':entity_type' => $entity_type)); + foreach ($this->entityConditions as $entity_condition) { + if ($entity_type != $entity_condition['entity_type']) { + throw new EntityFieldQueryException(t("Can't query multiple entity tables at once.")); + } + $query->condition($entity_condition['column'], $entity_condition['value'], $entity_condition['operator']); + } + foreach ($this->entityOrder as $order) { + $query->orderBy("$base_table." . $order['column'], $order['direction']); + } + if ($this->range) { + $query->range($this->range['start'], $this->range['length']); + } + $ids = array(); + foreach ($query->execute() as $entity_id) { + $ids[] = $entity_id->entity_id; + } + return $ids ? array($entity_type => entity_load($entity_type, $ids)) : array(); + } +} === modified file 'modules/field/field.api.php' --- modules/field/field.api.php 2010-05-01 00:06:44 +0000 +++ modules/field/field.api.php 2010-05-03 09:01:30 +0000 @@ -1361,21 +1361,26 @@ function hook_field_storage_delete_revis } /** - * Handle a field query. + * Handle a entity + field query. * - * @param $field_name - * The name of the field to query. - * @param $conditions - * See field_attach_query(). - * A storage module that doesn't support querying a given column should raise - * a FieldQueryException. Incompatibilities should be mentioned on the module - * project page. - * @param $options - * See field_attach_query(). All option keys are guaranteed to be specified. - * @return - * See field_attach_query(). + * @param $entity_types + * A list of entity types as passed to EntityFieldQuery::entityTypes(). + * @param $entity_conditions + * An list of entity conditions. Each entry maps to one call of + * EntityFieldQuery::entityConditions(). + * @param $field_conditions + * An list of field conditions. Each entry maps to one call of + * EntityFieldQuery::fieldConditions(). + * @param $field_order + * An list of field orders. Each entry maps to one call of + * EntityFieldQuery::fieldOrderBy(). + * @param $range + * The query range as passed to EntityFieldQuery::range(). + * @param $deleted + * Whether to delete with the deleted values as passed to + * EntityFieldQuery::deleted() */ -function hook_field_storage_query($field_name, $conditions, $options) { +function hook_field_storage_query($entity_types, $entity_conditions, $field_conditions, $field_order, $entity_order, $range, $deleted) { } /** @@ -1531,33 +1536,6 @@ function hook_field_storage_pre_update($ } /** - * Act before the storage backend runs the query. - * - * This hook should be implemented by modules that use - * hook_field_storage_pre_load(), hook_field_storage_pre_insert() and - * hook_field_storage_pre_update() to bypass the regular storage engine, to - * handle field queries. - * - * @param $field_name - * The name of the field to query. - * @param $conditions - * See field_attach_query(). - * A storage module that doesn't support querying a given column should raise - * a FieldQueryException. Incompatibilities should be mentioned on the module - * project page. - * @param $options - * See field_attach_query(). All option keys are guaranteed to be specified. - * @param $skip_field - * Boolean, always coming as FALSE. - * @return - * See field_attach_query(). - * The $skip_field parameter should be set to TRUE if the query has been - * handled. - */ -function hook_field_storage_pre_query($field_name, $conditions, $options, &$skip_field) { -} - -/** * @} End of "ingroup field_storage" */ @@ -1624,8 +1602,12 @@ function hook_field_update_field_forbid( // Identify the keys that will be lost. $lost_keys = array_diff(array_keys($field['settings']['allowed_values']), array_keys($prior_field['settings']['allowed_values'])); // If any data exist for those keys, forbid the update. - $count = field_attach_query($prior_field['id'], array('value', $lost_keys, 'IN'), 1); - if ($count > 0) { + $query = new EntityFieldQuery; + $found = $query + ->fieldCondition($prior_field['field_name'], 'value', $lost_keys) + ->range(0, 1) + ->execute(); + if ($found) { throw new FieldUpdateForbiddenException("Cannot update a list field not to include keys with existing data"); } } === modified file 'modules/field/field.attach.inc' --- modules/field/field.attach.inc 2010-04-04 12:48:18 +0000 +++ modules/field/field.attach.inc 2010-05-03 05:45:19 +0000 @@ -31,15 +31,6 @@ class FieldValidationException extends F } /** - * Exception thrown by field_attach_query() on unsupported query syntax. - * - * Some storage modules might not support the full range of the syntax for - * conditions, and will raise a FieldQueryException when an usupported - * condition was specified. - */ -class FieldQueryException extends FieldException {} - -/** * @defgroup field_storage Field Storage API * @{ * Implement a storage engine for Field API data. @@ -1042,132 +1033,6 @@ function field_attach_delete_revision($e } /** - * Retrieve entities matching a given set of conditions. - * - * Note that the query 'conditions' only apply to the stored values. - * In a regular field_attach_load() call, field values additionally go through - * hook_field_load() and hook_field_attach_load() invocations, which can add - * to or affect the raw stored values. The results of field_attach_query() - * might therefore differ from what could be expected by looking at a regular, - * fully loaded entity. - * - * @param $field_id - * The id of the field to query. - * @param $conditions - * An array of query conditions. Each condition is a numerically indexed - * array, in the form: array(column, value, operator). - * Not all storage engines are required to support queries on all column, or - * with all operators below. A FieldQueryException will be raised if an - * unsupported condition is specified. - * Supported columns: - * - any of the columns defined in hook_field_schema() for $field_name's - * field type: condition on field value, - * - 'type': condition on entity type (e.g. 'node', 'user'...), - * - 'bundle': condition on entity bundle (e.g. node type), - * - 'entity_id': condition on entity id (e.g node nid, user uid...), - * - 'deleted': condition on whether the field's data is - * marked deleted for the entity (defaults to FALSE if not specified) - * The field_attach_query_revisions() function additionally supports: - * - 'revision_id': condition on entity revision id (e.g node vid). - * Supported operators: - * - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'ENDS_WITH', - * 'CONTAINS': these operators expect the value as a literal of the same - * type as the column, - * - 'IN', 'NOT IN': this operator expects the value as an array of - * literals of the same type as the column. - * - 'BETWEEN': this operator expects the value as an array of two literals - * of the same type as the column. - * The operator can be ommitted, and will default to 'IN' if the value is - * an array, or to '=' otherwise. - * Example values for $conditions: - * @code - * array( - * array('type', 'node'), - * ); - * array( - * array('bundle', array('article', 'page')), - * array('value', 12, '>'), - * ); - * @endcode - * @param $options - * An associative array of additional options: - * - limit: The number of results that is requested. This is only a hint to - * the storage engine(s); callers should be prepared to handle fewer or - * more results. Specify FIELD_QUERY_NO_LIMIT to retrieve all available - * entities. This option has a default value of 0 so callers must make an - * explicit choice to potentially retrieve an enormous result set. - * - cursor: A reference to an opaque cursor that allows a caller to iterate - * through multiple result sets. On the first call, pass 0; the correct - * value to pass on the next call will be written into the value on return. - * When there is no more query data available, the value will be filled in - * with FIELD_QUERY_COMPLETE. If cursor is passed as NULL, the first result - * set is returned and no next cursor is returned. - * - count: If TRUE, return a single count of all matching entities; limit and - * cursor are ignored. - * - age: Internal use only. Use field_attach_query_revisions() instead of - * passing FIELD_LOAD_REVISION. - * - FIELD_LOAD_CURRENT (default): query the most recent revisions for all - * entities. The results will be keyed by entity type and entity id. - * - FIELD_LOAD_REVISION: query all revisions. The results will be keyed by - * entity type and entity revision id. - * @return - * An array keyed by entity type (e.g. 'node', 'user'...), then by entity id - * or revision id (depending of the value of the $age parameter). The values - * are pseudo-entities with the bundle, id, and revision id fields filled in. - * Throws a FieldQueryException if the field's storage doesn't support the - * specified conditions. - */ -function field_attach_query($field_id, $conditions, $options = array()) { - // Merge in default options. - $default_options = array( - 'limit' => 0, - 'cursor' => 0, - 'count' => FALSE, - 'age' => FIELD_LOAD_CURRENT, - ); - $options += $default_options; - - // Give a chance to 3rd party modules that bypass the storage engine to - // handle the query. - $skip_field = FALSE; - foreach (module_implements('field_storage_pre_query') as $module) { - $function = $module . '_field_storage_pre_query'; - $results = $function($field_id, $conditions, $options, $skip_field); - // Stop as soon as a module claims it handled the query. - if ($skip_field) { - break; - } - } - // If the request hasn't been handled, let the storage engine handle it. - if (!$skip_field) { - $field = field_info_field_by_id($field_id); - $function = $field['storage']['module'] . '_field_storage_query'; - $results = $function($field_id, $conditions, $options); - } - - return $results; -} - -/** - * Retrieve entity revisions matching a given set of conditions. - * - * See field_attach_query() for more informations. - * - * @param $field_id - * The id of the field to query. - * @param $conditions - * See field_attach_query(). - * @param $options - * An associative array of additional options. See field_attach_query(). - * @return - * See field_attach_query(). - */ -function field_attach_query_revisions($field_id, $conditions, $options = array()) { - $options['age'] = FIELD_LOAD_REVISION; - return field_attach_query($field_id, $conditions, $options); -} - -/** * Prepare field data prior to display. * * This function must be called before field_attach_view(). It lets field === modified file 'modules/field/field.crud.inc' --- modules/field/field.crud.inc 2010-04-24 07:19:09 +0000 +++ modules/field/field.crud.inc 2010-05-03 08:10:30 +0000 @@ -993,21 +993,17 @@ function field_purge_batch($batch_size) foreach ($instances as $instance) { $field = field_info_field_by_id($instance['field_id']); - // Retrieve some pseudo-entities. - $entity_types = field_attach_query($instance['field_id'], array(array('bundle', $instance['bundle']), array('deleted', 1)), array('limit' => $batch_size)); - - if (count($entity_types) > 0) { - // Field data for the instance still exists. - foreach ($entity_types as $entity_type => $entities) { - field_attach_load($entity_type, $entities, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1)); + // Retrieve some entities. + $query = new EntityFieldQuery; + $results = $query + ->fieldCondition($field['field_name'], 'bundle', $instance['bundle']) + ->deleted(TRUE) + ->range(0, $batch_size) + ->execute(); + if ($results) { + foreach ($results as $entity_type => $entities) { foreach ($entities as $id => $entity) { - // field_attach_query() may return more results than we asked for. - // Stop when he have done our batch size. - if ($batch_size-- <= 0) { - return; - } - // Purge the data for the entity. field_purge_data($entity_type, $entity, $field, $instance); } @@ -1119,4 +1115,3 @@ function field_purge_field($field) { /** * @} End of "defgroup field_purge". */ - === modified file 'modules/field/field.module' --- modules/field/field.module 2010-04-13 15:23:02 +0000 +++ modules/field/field.module 2010-05-03 07:42:36 +0000 @@ -102,28 +102,6 @@ define('FIELD_LOAD_CURRENT', 'FIELD_LOAD define('FIELD_LOAD_REVISION', 'FIELD_LOAD_REVISION'); /** - * @name Field query flags - * @{ - * Flags for field_attach_query(). - */ - -/** - * Limit argument for field_attach_query() to request all available - * entities instead of a limited number. - */ -define('FIELD_QUERY_NO_LIMIT', 'FIELD_QUERY_NO_LIMIT'); - -/** - * Cursor return value for field_attach_query() to indicate that no - * more data is available. - */ -define('FIELD_QUERY_COMPLETE', 'FIELD_QUERY_COMPLETE'); - -/** - * @} End of "Field query flags". - */ - -/** * Exception class thrown by hook_field_update_forbid(). */ class FieldUpdateForbiddenException extends FieldException {} @@ -653,8 +631,10 @@ function field_get_items($entity_type, $ * TRUE if the field has data for any entity; FALSE otherwise. */ function field_has_data($field) { - $results = field_attach_query($field['id'], array(), array('limit' => 1)); - return !empty($results); + $query = new EntityFieldQuery(); + $query->fieldCondition($field['field_name'], 'entity_id', 0, '>'); + $query->range(0, 1); + return (bool) $query->execute(); } /** === modified file 'modules/field/modules/field_sql_storage/field_sql_storage.module' --- modules/field/modules/field_sql_storage/field_sql_storage.module 2010-03-27 05:52:49 +0000 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 2010-05-03 09:51:26 +0000 @@ -67,7 +67,7 @@ function _field_sql_storage_revision_tab * table that is unique among all other fields. */ function _field_sql_storage_columnname($name, $column) { - return $name . '_' . $column; + return in_array($column, array('bundle', 'entity_id')) ? $column : $name . '_' . $column; } /** @@ -472,129 +472,122 @@ function field_sql_storage_field_storage /** * Implements hook_field_storage_query(). */ -function field_sql_storage_field_storage_query($field_id, $conditions, $options) { - $load_current = $options['age'] == FIELD_LOAD_CURRENT; - - $field = field_info_field_by_id($field_id); - $field_name = $field['field_name']; - $table = $load_current ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field); - $field_columns = array_keys($field['columns']); - - // Build the query. - $query = db_select($table, 't'); - $query->join('field_config_entity_type', 'e', 't.etid = e.etid'); - - // Add conditions. - foreach ($conditions as $condition) { - // A condition is either a (column, value, operator) triple, or a - // (column, value) pair with implied operator. - @list($column, $value, $operator) = $condition; - // Translate operator and value if needed. - switch ($operator) { - case 'STARTS_WITH': - $operator = 'LIKE'; - $value = db_like($value) . '%'; - break; - - case 'ENDS_WITH': - $operator = 'LIKE'; - $value = '%' . db_like($value); - break; - - case 'CONTAINS': - $operator = 'LIKE'; - $value = '%' . db_like($value) . '%'; - break; - } - // Translate field columns into prefixed db columns. - if (in_array($column, $field_columns)) { - $column = _field_sql_storage_columnname($field_name, $column); - } - // Translate entity types into numeric ids. Expressing the condition on the - // local 'etid' column rather than the JOINed 'type' column avoids a - // filesort. - if ($column == 'type') { - $column = 't.etid'; - if (is_array($value)) { - foreach (array_keys($value) as $key) { - $value[$key] = _field_sql_storage_etid($value[$key]); +function field_sql_storage_field_storage_query($entity_types, $entity_conditions, $field_conditions, $field_order, $entity_order, $range, $deleted) { + $groups = array(); + foreach ($field_conditions as $key => $condition) { + $field = field_info_field($condition['field_name']); + $tablename = _field_sql_storage_tablename($field); + if (isset($query)) { + // Every condition needs a new table. + $table_alias = $query->join($tablename, $tablename, "$tablename.etid = $field_base_table.etid AND $tablename.entity_id = $field_base_table.entity_id"); + } + else { + $query = db_select($tablename); + $query->addField($tablename, 'entity_id', 'entity_id'); + // As only a numeric ID is stored instead of the entity type so add the + // field_config_entity_type table to resolve the etid to a more readable + // name. + $query->join('field_config_entity_type', 'fcet', "fcet.etid = $tablename.etid"); + $query->addField('fcet', 'type', 'entity_type'); + $table_alias = $field_base_table = $tablename; + } + $columnname = _field_sql_storage_columnname($condition['field_name'], $condition['column']); + // Add the condition itself. + $query->condition("$table_alias.$columnname", $condition['value'], $condition['operator']); + foreach (array('delta', 'language') as $column) { + if (isset($condition[$column .'_group'])) { + $group_name = $condition[$column .'_group']; + if (!isset($groups[$column][$group_name])) { + $groups[$column][$group_name] = $table_alias; + } + else { + $query->where("$table_alias.$column = " . $groups[$column][$group_name] . ".$column"); } } - else { - $value = _field_sql_storage_etid($value); - } - } - // Track condition on 'deleted'. - if ($column == 'deleted') { - $condition_deleted = TRUE; - } - - $query->condition($column, $value, $operator); - } - - // Exclude deleted data unless we have a condition on it. - if (!isset($condition_deleted)) { - $query->condition('deleted', 0); - } - - // For a count query, return the count now. - if ($options['count']) { - return $query - ->fields('t', array('etid', 'entity_id', 'revision_id')) - ->distinct() - ->countQuery() - ->execute() - ->fetchField(); - } - - // For a data query, add fields. - $query - ->fields('t', array('bundle', 'entity_id', 'revision_id')) - ->fields('e', array('type')) - // We need to ensure entities arrive in a consistent order for the - // range() operation to work. - ->orderBy('t.etid') - ->orderBy('t.entity_id'); - - // Initialize results array - $return = array(); - - // Getting $count entities possibly requires reading more than $count rows - // since fields with multiple values span over several rows. We query for - // batches of $count rows until we've either read $count entities or received - // less rows than asked for. - $entity_count = 0; - do { - if ($options['limit'] != FIELD_QUERY_NO_LIMIT) { - $query->range($options['cursor'], $options['limit']); } - $results = $query->execute(); - - $row_count = 0; - foreach ($results as $row) { - $row_count++; - $options['cursor']++; - // If querying all revisions and the entity type has revisions, we need - // to key the results by revision_ids. - $entity_type = entity_get_info($row->type); - $id = ($load_current || empty($entity_type['entity keys']['revision'])) ? $row->entity_id : $row->revision_id; - - if (!isset($return[$row->type][$id])) { - $return[$row->type][$id] = entity_create_stub_entity($row->type, array($row->entity_id, $row->revision_id, $row->bundle)); - $entity_count++; + } + if ($entity_conditions) { + $entity_type = ''; + foreach ($entity_conditions as $entity_condition) { + if (!$entity_type) { + $entity_type = $entity_condition['entity_type']; + $entity_base_table = _field_sql_storage_query_join_entity($query, $entity_type, $field_base_table); + } + elseif ($entity_type != $entity_condition['entity_type']) { + throw new EntityFieldQueryException(t("Can't have conditions on two different entity types")); } + $query->condition("$entity_base_table." . $entity_condition['column'], $entity_condition['value'], isset($entity_condition['operator']) ? $entity_condition['operator'] : NULL); } - } while ($options['limit'] != FIELD_QUERY_NO_LIMIT && $row_count == $options['limit'] && $entity_count < $options['limit']); - - // The query is complete when the last batch returns less rows than asked - // for. - if ($row_count < $options['limit']) { - $options['cursor'] = FIELD_QUERY_COMPLETE; + $entity_types = $entity_type; + } + // $entity_types might be an array passing to EntityFieldQuery::entityTypes() + // or it might be the single entity type from the entity conditions. DBTNG + // does the right thing for an array (IN) and a string (=) both if the + // operator is not specified, which is exactly what we need. The + // necessary table was added when the $query was created. + if ($entity_types) { + $query->condition('fcet.type', $entity_types); + } + foreach ($entity_order as $order) { + if (!isset($entity_base_table)) { + $entity_base_table = _field_sql_storage_query_join_entity($query, $entity_types[0], $field_base_table); + } + $query->orderBy("$entity_base_table." . $order['column'], $order['direction']); } + foreach ($field_order as $order) { + $columnname = _field_sql_storage_columnname($order['field_name'], $order['column']); + $field = field_info_field($order['field_name']); + $tablename = _field_sql_storage_tablename($field); + $query->orderBy("$tablename.$columnname", $order['direction']); + } + if ($range) { + $query->range($range['start'], $range['length']); + } + if (isset($deleted)) { + $query->condition("$table_alias.deleted", (int) $deleted); + } + debug(strtr($query, $query->getArguments() + array('}' => '', '{' => ''))); + $entities = array(); + $all_ids = array(); + foreach ($query->execute() as $partial_entity) { + $all_ids[$partial_entity->entity_type][] = $partial_entity->entity_id; + } + foreach ($all_ids as $entity_type => $ids) { + $entities[$entity_type] = entity_load($entity_type, $ids); + } + return $entities; +} - return $return; +/** + * Add the base entity table to a field query object. + * + * @param $query + * A SelectQuery containing at least one table as + * specified by _field_sql_storage_tablename(). + * @param $entity_type + * The entity type for which the base table should be joined. + * @param $field_base_table + * Name of a table in $query. As only INNER JOINs are used, it does not + * matter which. + */ +function _field_sql_storage_query_join_entity(SelectQuery $query, $entity_type, $field_base_table) {; + $entity_info = entity_get_info($entity_type); + $entity_base_table = isset($entity_info['base_table']) ? $entity_info['base_table'] : $entity_type; + // Figure out whether the entity is revisioned or not and JOIN based on this + // information. + if (empty($entity_info['entity keys']['revision'])) { + $entity_field = $entity_info['entity keys']['id']; + $field = 'entity_id'; + } + else { + $entity_field = $entity_info['entity keys']['revision']; + $field = 'revision_id'; + } + $query->join($entity_base_table, $entity_base_table, "$entity_base_table.$entity_field = $field_base_table.$field"); + return $entity_base_table; } + /** * Implements hook_field_storage_delete_revision(). * === modified file 'modules/field/modules/list/list.module' --- modules/field/modules/list/list.module 2010-03-27 12:49:32 +0000 +++ modules/field/modules/list/list.module 2010-05-03 09:02:17 +0000 @@ -101,7 +101,7 @@ function list_field_schema($field) { * * @todo: If $has_data, add a form validate function to verify that the * new allowed values do not exclude any keys for which data already - * exists in the databae (use field_attach_query()) to find out. + * exists in the field storage (use EntityFieldQuery) to find out. * Implement the validate function via hook_field_update_forbid() so * list.module does not depend on form submission. */ === modified file 'modules/field/tests/field.test' --- modules/field/tests/field.test 2010-05-01 08:12:22 +0000 +++ modules/field/tests/field.test 2010-05-03 08:16:21 +0000 @@ -623,215 +623,6 @@ class FieldAttachStorageTestCase extends $this->assertFalse(field_read_instance('test_entity', $this->field_name, $this->instance['bundle']), "First field is deleted"); $this->assertFalse(field_read_instance('test_entity', $field_name, $instance['bundle']), "Second field is deleted"); } - - /** - * Test field_attach_query(). - */ - function testFieldAttachQuery() { - $cardinality = $this->field['cardinality']; - $langcode = LANGUAGE_NONE; - - // Create an additional bundle with an instance of the field. - field_test_create_bundle('test_bundle_1', 'Test Bundle 1'); - $this->instance2 = $this->instance; - $this->instance2['bundle'] = 'test_bundle_1'; - field_create_instance($this->instance2); - - // Create instances of both fields on the second entity type. - $instance = $this->instance; - $instance['entity_type'] = 'test_cacheable_entity'; - field_create_instance($instance); - $instance2 = $this->instance2; - $instance2['entity_type'] = 'test_cacheable_entity'; - field_create_instance($instance2); - - // Unconditional count query returns 0. - $count = field_attach_query($this->field_id, array(), array('count' => TRUE)); - $this->assertEqual($count, 0, t('With no entities, count query returns 0.')); - - // Create two test entities, using two different types and bundles. - $entity_types = array(1 => 'test_entity', 2 => 'test_cacheable_entity'); - $entities = array(1 => field_test_create_stub_entity(1, 1, 'test_bundle'), 2 => field_test_create_stub_entity(2, 2, 'test_bundle_1')); - - // Create first test entity with random (distinct) values. - $values = array(); - for ($delta = 0; $delta < $cardinality; $delta++) { - do { - $value = mt_rand(1, 127); - } while (in_array($value, $values)); - $values[$delta] = $value; - $entities[1]->{$this->field_name}[$langcode][$delta] = array('value' => $values[$delta]); - } - field_attach_insert($entity_types[1], $entities[1]); - - // Unconditional count query returns 1. - $count = field_attach_query($this->field_id, array(), array('count' => TRUE)); - $this->assertEqual($count, 1, t('With one entity, count query returns @count.', array('@count' => $count))); - - // Create second test entity, sharing a value with the first one. - $common_value = $values[$cardinality - 1]; - $entities[2]->{$this->field_name} = array($langcode => array(array('value' => $common_value))); - field_attach_insert($entity_types[2], $entities[2]); - - // Query on the entity's values. - for ($delta = 0; $delta < $cardinality; $delta++) { - $conditions = array(array('value', $values[$delta])); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]), t('Query on value %delta returns the entity', array('%delta' => $delta))); - - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, ($values[$delta] == $common_value) ? 2 : 1, t('Count query on value %delta counts %count entities', array('%delta' => $delta, '%count' => $count))); - } - - // Query on a value that is not in the entity. - do { - $different_value = mt_rand(1, 127); - } while (in_array($different_value, $values)); - $conditions = array(array('value', $different_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertFalse(isset($result[$entity_types[1]][1]), t("Query on a value that is not in the entity doesn't return the entity")); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 0, t("Count query on a value that is not in the entity doesn't count the entity")); - - // Query on the value shared by both entities, and discriminate using - // additional conditions. - - $conditions = array(array('value', $common_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]) && isset($result[$entity_types[2]][2]), t('Query on a value common to both entities returns both entities')); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 2, t('Count query on a value common to both entities counts both entities')); - - $conditions = array(array('type', $entity_types[1]), array('value', $common_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both entities and a 'type' condition only returns the relevant entity")); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 1, t("Count query on a value common to both entities and a 'type' condition only returns the relevant entity")); - - $conditions = array(array('bundle', $entities[1]->fttype), array('value', $common_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both entities and a 'bundle' condition only returns the relevant entity")); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 1, t("Count query on a value common to both entities and a 'bundle' condition only counts the relevant entity")); - - $conditions = array(array('entity_id', $entities[1]->ftid), array('value', $common_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both entities and an 'entity_id' condition only returns the relevant entity")); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 1, t("Count query on a value common to both entities and an 'entity_id' condition only counts the relevant entity")); - - // Test result format. - $conditions = array(array('value', $values[0])); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $expected = array( - $entity_types[1] => array( - $entities[1]->ftid => field_test_create_stub_entity($entities[1]->ftid, $entities[1]->ftvid), - ) - ); - $this->assertEqual($result, $expected, t('Result format is correct.')); - - // Now test the count/offset paging capability. - - // Create a new bundle with an instance of the field. - field_test_create_bundle('offset_bundle', 'Offset Test Bundle'); - $this->instance2 = $this->instance; - $this->instance2['bundle'] = 'offset_bundle'; - field_create_instance($this->instance2); - - // Create 20 test entities, using the new bundle, but with - // non-sequential ids so we can tell we are getting the right ones - // back. We do not need unique values since field_attach_query() - // won't return them anyway. - $offset_entities = array(); - $offset_id = mt_rand(1, 3); - for ($i = 0; $i < 20; ++$i) { - $offset_id += mt_rand(2, 5); - $offset_entities[$offset_id] = field_test_create_stub_entity($offset_id, $offset_id, 'offset_bundle'); - $offset_entities[$offset_id]->{$this->field_name}[$langcode][0] = array('value' => $offset_id); - field_attach_insert('test_entity', $offset_entities[$offset_id]); - } - - // Query for the offset entities in batches, making sure we get - // back the right ones. - $cursor = 0; - foreach (array(1 => 1, 3 => 3, 5 => 5, 8 => 8, 13 => 3) as $count => $expect) { - $found = field_attach_query($this->field_id, array(array('bundle', 'offset_bundle')), array('limit' => $count, 'cursor' => &$cursor)); - if (isset($found['test_entity'])) { - $this->assertEqual(count($found['test_entity']), $expect, t('Requested @count, expected @expect, got @found, cursor @cursor', array('@count' => $count, '@expect' => $expect, '@found' => count($found['test_entity']), '@cursor' => $cursor))); - foreach ($found['test_entity'] as $id => $entity) { - $this->assert(isset($offset_entities[$id]), "Entity $id found"); - unset($offset_entities[$id]); - } - } - else { - $this->assertEqual(0, $expect, t('Requested @count, expected @expect, got @found, cursor @cursor', array('@count' => $count, '@expect' => $expect, '@found' => 0, '@cursor' => $cursor))); - } - } - $this->assertEqual(count($offset_entities), 0, "All entities found"); - $this->assertEqual($cursor, FIELD_QUERY_COMPLETE, "Cursor is FIELD_QUERY_COMPLETE"); - } - - /** - * Test field_attach_query_revisions(). - */ - function testFieldAttachQueryRevisions() { - $cardinality = $this->field['cardinality']; - - // Create first entity revision with random (distinct) values. - $entity_type = 'test_entity'; - $entities = array(1 => field_test_create_stub_entity(1, 1), 2 => field_test_create_stub_entity(1, 2)); - $langcode = LANGUAGE_NONE; - $values = array(); - for ($delta = 0; $delta < $cardinality; $delta++) { - do { - $value = mt_rand(1, 127); - } while (in_array($value, $values)); - $values[$delta] = $value; - $entities[1]->{$this->field_name}[$langcode][$delta] = array('value' => $values[$delta]); - } - field_attach_insert($entity_type, $entities[1]); - - // Create second entity revision, sharing a value with the first one. - $common_value = $values[$cardinality - 1]; - $entities[2]->{$this->field_name}[$langcode][0] = array('value' => $common_value); - field_attach_update($entity_type, $entities[2]); - - // Query on the entity values. - for ($delta = 0; $delta < $cardinality; $delta++) { - $conditions = array(array('value', $values[$delta])); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_type][1]), t('Query on value %delta returns the entity', array('%delta' => $delta))); - } - - // Query on a value that is not in the entity. - do { - $different_value = mt_rand(1, 127); - } while (in_array($different_value, $values)); - $conditions = array(array('value', $different_value)); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertFalse(isset($result[$entity_type][1]), t("Query on a value that is not in the entity doesn't return the entity")); - - // Query on the value shared by both entities, and discriminate using - // additional conditions. - - $conditions = array(array('value', $common_value)); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_type][1]) && isset($result[$entity_type][2]), t('Query on a value common to both entities returns both entities')); - - $conditions = array(array('revision_id', $entities[1]->ftvid), array('value', $common_value)); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_type][1]) && !isset($result[$entity_type][2]), t("Query on a value common to both entities and a 'revision_id' condition only returns the relevant entity")); - - // Test FIELD_QUERY_RETURN_IDS result format. - $conditions = array(array('value', $values[0])); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $expected = array( - $entity_type => array( - $entities[1]->ftid => field_test_create_stub_entity($entities[1]->ftid, $entities[1]->ftvid), - ) - ); - $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_IDS result format returns the expect result')); - } } /** @@ -2977,7 +2768,7 @@ class FieldBulkDeleteTestCase extends Fi * the database and that the appropriate Field API functions can * operate on the deleted data and instance. * - * This tests how field_attach_query() interacts with + * This tests how EntityFieldQuery interacts with * field_delete_instance() and could be moved to FieldCrudTestCase, * but depends on this class's setUp(). */ @@ -2986,7 +2777,10 @@ class FieldBulkDeleteTestCase extends Fi $field = reset($this->fields); // There are 10 entities of this bundle. - $found = field_attach_query($field['id'], array(array('bundle', $bundle)), array('limit' => FIELD_QUERY_NO_LIMIT)); + $query = new EntityFieldQuery; + $found = $query + ->fieldCondition($field['field_name'], 'bundle', $bundle) + ->execute(); $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found before deleting'); // Delete the instance. @@ -2999,13 +2793,17 @@ class FieldBulkDeleteTestCase extends Fi $this->assertEqual($instances[0]['bundle'], $bundle, 'The deleted instance is for the correct bundle'); // There are 0 entities of this bundle with non-deleted data. - $found = field_attach_query($field['id'], array(array('bundle', $bundle)), array('limit' => FIELD_QUERY_NO_LIMIT)); + $found = $query + ->fieldCondition($field['field_name'], 'bundle', $bundle) + ->execute(); $this->assertTrue(!isset($found['test_entity']), 'No entities found after deleting'); // There are 10 entities of this bundle when deleted fields are allowed, and // their values are correct. - $found = field_attach_query($field['id'], array(array('bundle', $bundle), array('deleted', 1)), array('limit' => FIELD_QUERY_NO_LIMIT)); - field_attach_load($this->entity_type, $found[$this->entity_type], FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1)); + $found = $query + ->fieldCondition($field['field_name'], 'bundle', $bundle) + ->deleted(TRUE) + ->execute(); $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found after deleting'); foreach ($found['test_entity'] as $id => $entity) { $this->assertEqual($this->entities[$id]->{$field['field_name']}, $entity->{$field['field_name']}, "Entity $id with deleted data loaded correctly"); @@ -3036,7 +2834,10 @@ class FieldBulkDeleteTestCase extends Fi field_purge_batch($batch_size); // There are $count deleted entities left. - $found = field_attach_query($field['id'], array(array('bundle', $bundle), array('deleted', 1)), array('limit' => FIELD_QUERY_NO_LIMIT)); + $found = $query + ->fieldCondition($field['field_name'], 'bundle', $bundle) + ->deleted(TRUE) + ->execute(); $this->assertEqual($count ? count($found['test_entity']) : count($found), $count, 'Correct number of entities found after purging 2'); } === modified file 'modules/field/tests/field_test.entity.inc' --- modules/field/tests/field_test.entity.inc 2010-03-27 18:41:13 +0000 +++ modules/field/tests/field_test.entity.inc 2010-05-03 09:48:30 +0000 @@ -46,6 +46,29 @@ function field_test_entity_info() { 'bundles' => $bundles, 'view modes' => $test_entity_modes, ), + 'test_entity_1' => array( + 'name' => t('Test Entity 1'), + 'base table' => 'test_entity_1', + 'fieldable' => TRUE, + 'field cache' => FALSE, + 'entity keys' => array( + 'id' => 'ftid', + 'bundle' => 'fttype', + ), + 'bundles' => array('bundle1' => array('label' => 'Bundle1'), 'bundle2' => array('label' => 'Bundle2')), + 'view modes' => $test_entity_modes, + ), + 'test_entity_2' => array( + 'name' => t('Test Entity 2'), + 'base table' => 'test_entity_2', + 'fieldable' => TRUE, + 'field cache' => FALSE, + 'entity keys' => array( + 'id' => 'ftid', + ), + 'bundles' => array('test_entity_2' => array('label' => 'Test entity 2')), + 'view modes' => $test_entity_modes, + ), ); } === modified file 'modules/field/tests/field_test.field.inc' --- modules/field/tests/field_test.field.inc 2010-03-12 19:51:40 +0000 +++ modules/field/tests/field_test.field.inc 2010-05-03 09:24:02 +0000 @@ -26,6 +26,14 @@ function field_test_field_info() { 'default_widget' => 'test_field_widget', 'default_formatter' => 'field_test_default', ), + 'shape' => array( + 'label' => t('Shape'), + 'description' => t('Another dummy field.'), + 'settings' => array(), + 'instance_settings' => array(), + 'default_widget' => 'test_field_widget', + 'default_formatter' => 'field_test_default', + ), ); } @@ -33,18 +41,36 @@ function field_test_field_info() { * Implements hook_field_schema(). */ function field_test_field_schema($field) { - return array( - 'columns' => array( - 'value' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => FALSE, + if ($field['type'] == 'test_field') { + return array( + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'size' => 'medium', + 'not null' => FALSE, + ), ), - ), - 'indexes' => array( - 'value' => array('value'), - ), - ); + 'indexes' => array( + 'value' => array('value'), + ), + ); + } + else { + return array( + 'columns' => array( + 'shape' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => FALSE, + ), + 'color' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => FALSE, + ), + ), + ); + } } /** @@ -63,7 +89,7 @@ function field_test_field_load($entity_t foreach ($items as $id => $item) { // To keep the test non-intrusive, only act for instances with the // test_hook_field_load setting explicitly set to TRUE. - if ($instances[$id]['settings']['test_hook_field_load']) { + if (isset($instances[$id]['settings']['test_hook_field_load'])) { foreach ($item as $delta => $value) { // Don't add anything on empty values. if ($value) { === modified file 'modules/field/tests/field_test.install' --- modules/field/tests/field_test.install 2009-12-04 16:49:45 +0000 +++ modules/field/tests/field_test.install 2010-05-03 09:14:41 +0000 @@ -41,7 +41,7 @@ function field_test_schema() { 'description' => 'The type of this test_entity.', 'type' => 'varchar', 'length' => 32, - 'not null' => TRUE, + 'not null' => FALSE, 'default' => '', ), ), @@ -50,6 +50,37 @@ function field_test_schema() { ), 'primary key' => array('ftid'), ); + $schema['test_entity_1'] = array( + 'description' => 'The base table for test_entities.', + 'fields' => array( + 'ftid' => array( + 'description' => 'The {test_entity} this version belongs to.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'fttype' => array( + 'description' => 'The type of this test_entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => FALSE, + 'default' => '', + ), + ), + ); + $schema['test_entity_2'] = array( + 'description' => 'Stores information about each saved version of a {test_entity}.', + 'fields' => array( + 'ftid' => array( + 'description' => 'The {test_entity} this version belongs to.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + ); $schema['test_entity_revision'] = array( 'description' => 'Stores information about each saved version of a {test_entity}.', 'fields' => array( === modified file 'modules/simpletest/drupal_web_test_case.php' --- modules/simpletest/drupal_web_test_case.php 2010-05-01 08:12:22 +0000 +++ modules/simpletest/drupal_web_test_case.php 2010-05-02 07:48:17 +0000 @@ -1279,7 +1279,7 @@ class DrupalWebTestCase extends DrupalTe $schema = drupal_get_schema(NULL, TRUE); $ret = array(); foreach ($schema as $name => $table) { - db_drop_table($name); +# db_drop_table($name); } // Return the database prefix to the original. === modified file 'modules/simpletest/simpletest.info' --- modules/simpletest/simpletest.info 2010-02-03 18:16:22 +0000 +++ modules/simpletest/simpletest.info 2010-05-03 09:04:29 +0000 @@ -19,6 +19,7 @@ files[] = tests/bootstrap.test files[] = tests/cache.test files[] = tests/common.test files[] = tests/database_test.test +files[] = tests/entity_query.test files[] = tests/error.test files[] = tests/file.test files[] = tests/filetransfer.test === added file 'modules/simpletest/tests/entity_query.test' --- modules/simpletest/tests/entity_query.test 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/entity_query.test 2010-05-03 09:54:39 +0000 @@ -0,0 +1,304 @@ + 'Entity query', + 'description' => 'Test the EntityFieldQuery class.', + 'group' => 'Entity API', + ); + } + + function setUp() { + parent::setUp(array('field_test')); + + field_attach_create_bundle('bundle1', 'test_entity_1'); + field_attach_create_bundle('bundle2', 'test_entity_1'); + field_attach_create_bundle('test_entity_2', 'test_entity_2'); + + $instances = array(); + $this->fields = array(); + $this->fields[0] = $field_name = drupal_strtolower($this->randomName() . '_field_name'); + $field = array('field_name' => $field_name, 'type' => 'test_field', 'cardinality' => 4); + $field = field_create_field($field); + $field_id = $field['id']; + $instance = array( + 'field_name' => $field_name, + 'entity_type' => '', + 'bundle' => '', + 'label' => $this->randomName() . '_label', + 'description' => $this->randomName() . '_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + 'widget' => array( + 'type' => 'test_field_widget', + 'label' => 'Test Field', + 'settings' => array( + 'test_widget_setting' => $this->randomName(), + ) + ) + ); + $instances[0] = $instance; + + + // Add an instance to that bundle. + $instances[0]['bundle'] = 'bundle1'; + $instances[0]['entity_type'] = 'test_entity_1'; + field_create_instance($instances[0]); + $instances[0]['bundle'] = $instances[0]['entity_type'] = 'test_entity_2'; + field_create_instance($instances[0]); + + $this->fields[1] = $field_name = drupal_strtolower($this->randomName() . '_field_name'); + $field = array('field_name' => $field_name, 'type' => 'shape', 'cardinality' => 4); + $field = field_create_field($field); + $field_id = $field['id']; + $instance = array( + 'field_name' => $field_name, + 'entity_type' => '', + 'bundle' => '', + 'label' => $this->randomName() . '_label', + 'description' => $this->randomName() . '_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + 'widget' => array( + 'type' => 'test_field_widget', + 'label' => 'Test Field', + 'settings' => array( + 'test_widget_setting' => $this->randomName(), + ) + ) + ); + $instances[1] = $instance; + + // Add an instance to that bundle. + $instances[1]['bundle'] = 'bundle1'; + $instances[1]['entity_type'] = 'test_entity_1'; + field_create_instance($instances[1]); + $instances[1]['bundle'] = $instances[1]['entity_type'] = 'test_entity_2'; + field_create_instance($instances[1]); + + // Write entity base table if there is one. + $entities = array(); + + // First, of type test_entity_1 + for ($i = 1; $i < 5; $i++) { + $entity = new stdClass; + $entity->ftid = $i; + $entity->fttype = 'bundle1'; + $entity->{$this->fields[0]}[LANGUAGE_NONE][0]['value'] = $i; + drupal_write_record('test_entity_1', $entity); + field_attach_insert('test_entity_1', $entity); + } + $entity = new stdClass; + $entity->ftid = 5; + $entity->fttype = 'bundle1'; + $entity->{$this->fields[1]}[LANGUAGE_NONE][0]['shape'] = 'square'; + $entity->{$this->fields[1]}[LANGUAGE_NONE][0]['color'] = 'red'; + $entity->{$this->fields[1]}[LANGUAGE_NONE][1]['shape'] = 'circle'; + $entity->{$this->fields[1]}[LANGUAGE_NONE][1]['color'] = 'blue'; + drupal_write_record('test_entity_2', $entity); + field_attach_insert('test_entity_2', $entity); + } + + /** + * Test field_attach_query(). + */ + function testEntityFieldQuery() { + // First, test without options. + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 3, '='); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 3), + ), t('Test the "equal to" operation.')); + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 2, '>'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Test the "greater than" operation.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', array(3, 4), 'IN'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Test the "in" operation.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', array(1, 3), 'BETWEEN'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 1), + array('test_entity_1', 2), + array('test_entity_1', 3), + ), t('Test the "between" operation.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 3); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 3), + ), t('Test omission of an operator with a single item.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', array(3, 4)); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Test omission of an operator with multiple items.')); + + $query = new EntityFieldQuery(); + $query->entityCondition('test_entity_1', 'ftid', 1); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 1), + ), t('Test entity conditions.')); + + $query = new EntityFieldQuery(); + $query->entityCondition('test_entity_1', 'ftid', 1, '>'); + $query->fieldCondition($this->fields[0], 'value', 4, '<'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 2), + array('test_entity_1', 3), + ), t('Test entity and field conditions.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 0, '>'); + $query->fieldOrderBy($this->fields[0], 'value', 'asc'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 1), + array('test_entity_1', 2), + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Field sort: ascending.'), TRUE); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 0, '>'); + $query->fieldOrderBy($this->fields[0], 'value', 'desc'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 4), + array('test_entity_1', 3), + array('test_entity_1', 2), + array('test_entity_1', 1), + ), t('Field sort: descending.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityTypes(array('test_entity_1')); + $query->entityOrderBy('ftid', 'asc'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 1), + array('test_entity_1', 2), + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Entity sort: ascending.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityTypes(array('test_entity_1')); + $query->entityOrderBy('ftid', 'desc'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 4), + array('test_entity_1', 3), + array('test_entity_1', 2), + array('test_entity_1', 1), + ), t('Entity sort: descending.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityTypes(array('test_entity_1')); + $query->entityOrderBy('ftid', 'asc'); + $query->range(0, 2); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 1), + array('test_entity_1', 2), + ), t('Test limit.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityTypes(array('test_entity_1')); + $query->entityOrderBy('ftid', 'asc'); + $query->range(2, 4); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Test offset.'), TRUE); + + for ($i = 6; $i < 10; $i++) { + $entity = new stdClass; + $entity->ftid = $i; + $entity->{$this->fields[0]}[LANGUAGE_NONE][0]['value'] = $i - 5; + drupal_write_record('test_entity_2', $entity); + field_attach_insert('test_entity_2', $entity); + } + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 2, '>'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_1', 3), + array('test_entity_1', 4), + array('test_entity_2', 8), + array('test_entity_2', 9), + ), t('Select a field across multiple entities.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[1], 'shape', 'square'); + $query->fieldCondition($this->fields[1], 'color', 'blue'); + $this->EntityFieldQueryHelper($query, array( + array('test_entity_2', 5), + ), t('Test without a delta group.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[1], 'shape', 'square', '=', 'group'); + $query->fieldCondition($this->fields[1], 'color', 'blue', '=', 'group'); + $this->EntityFieldQueryHelper($query, array(), t('Test with a delta group.')); + + $pass = FALSE; + $query = new EntityFieldQuery(); + try { + $query->execute(); + } + catch (EntityFieldQueryException $exception) { + $pass = ($exception->getMessage() == t('Empty query')); + } + $this->assertTrue($pass, t("Can't query the universe.")); + } + + /** + * Fetch the results of an EntityFieldQuery and compare. + * + * @param $query + * An EntityFieldQuery to run. + * @param $intended_results + * A list of results, every entry is again a list, first being the entity + * type, the second being the entity_id. + * @param $message + * The message to be displayed as the result of this test. + * @param $ordered + * If FALSE then the result of EntityFieldQuery will match + * $intended_results even if the order is not the same. If TRUE then order + * should match too. + */ + function EntityFieldQueryHelper($query, $intended_results, $message, $ordered = FALSE) { + $results = array(); + foreach ($query->execute() as $entity_type => $entities) { + foreach ($entities as $entity_id => $entity) { + // These should be the test entities where that field is 2 or more. + $results[] = array($entity_type, $entity_id); + } + } + if (!isset($ordered) || !$ordered) { + sort($results); + sort($intended_results); + } + $this->assertEqual($results, $intended_results, $message); + } +} === modified file 'modules/system/system.api.php' --- modules/system/system.api.php 2010-04-30 19:21:52 +0000 +++ modules/system/system.api.php 2010-05-03 08:47:42 +0000 @@ -268,6 +268,36 @@ function hook_entity_update($entity, $ty } /** + * Handle a entity + field query. + * + * This hook can change the behaviour EntityFieldQuery as necessary, see the + * return value of the function why. + * + * @param $entity_types + * A list of entity types as passed to EntityFieldQuery::entityTypes(). + * @param $entity_conditions + * An list of entity conditions. Each entry maps to one call of + * EntityFieldQuery::entityConditions(). + * @param $field_conditions + * An list of field conditions. Each entry maps to one call of + * EntityFieldQuery::fieldConditions(). + * @param $field_order + * An list of field orders. Each entry maps to one call of + * EntityFieldQuery::fieldOrderBy(). + * @param $range + * The query range as passed to EntityFieldQuery::range(). + * @param $deleted + * Whether to delete with the deleted values as passed to + * EntityFieldQuery::deleted(). + * @return + * If anything but NULL is returned that's going to be the return value of + * EntityFieldQuery::execute() immediately, so hook_field_storage_query won't + * be fired automatically. + */ +function hook_entity_query($entity_types, $entity_conditions, $field_conditions, $field_order, $entity_order, $range, $deleted) { +} + +/** * Define administrative paths. * * Modules may specify whether or not the paths they define in hook_menu() are === modified file 'modules/update/update.fetch.inc' --- modules/update/update.fetch.inc 2010-05-01 08:12:22 +0000 +++ modules/update/update.fetch.inc 2010-05-02 07:48:17 +0000 @@ -29,7 +29,7 @@ function update_manual_status() { * Process a step in the batch for fetching available update data. */ function update_fetch_data_batch(&$context) { - $queue = DrupalQueue::get('update_fetch_tasks'); + $queue = DrupalQueue::get('update_fetch_tasks', TRUE); if (empty($context['sandbox']['max'])) { $context['finished'] = 0; $context['sandbox']['max'] = $queue->numberOfItems(); @@ -100,7 +100,7 @@ function update_fetch_data_finished($suc * Attempt to drain the queue of tasks for release history data to fetch. */ function _update_fetch_data() { - $queue = DrupalQueue::get('update_fetch_tasks'); + $queue = DrupalQueue::get('update_fetch_tasks', TRUE); $end = time() + variable_get('update_max_fetch_time', UPDATE_MAX_FETCH_TIME); while (time() < $end && ($item = $queue->claimItem())) { _update_process_fetch_task($item->data); @@ -236,7 +236,7 @@ function _update_create_fetch_task($proj } $cid = 'fetch_task::' . $project['name']; if (empty($fetch_tasks[$cid])) { - $queue = DrupalQueue::get('update_fetch_tasks'); + $queue = DrupalQueue::get('update_fetch_tasks', TRUE); $queue->createItem($project); db_insert('cache_update') ->fields(array( === modified file 'modules/update/update.install' --- modules/update/update.install 2010-01-15 10:12:36 +0000 +++ modules/update/update.install 2010-05-01 05:16:28 +0000 @@ -71,7 +71,7 @@ function update_schema() { * Implements hook_install(). */ function update_install() { - $queue = DrupalQueue::get('update_fetch_tasks'); + $queue = DrupalQueue::get('update_fetch_tasks', TRUE); $queue->createQueue(); } @@ -92,7 +92,7 @@ function update_uninstall() { foreach ($variables as $variable) { variable_del($variable); } - $queue = DrupalQueue::get('update_fetch_tasks'); + $queue = DrupalQueue::get('update_fetch_tasks', TRUE); $queue->deleteQueue(); } @@ -164,6 +164,6 @@ function _update_requirement_check($proj */ function update_update_7000() { module_load_include('inc', 'system', 'system.queue'); - $queue = DrupalQueue::get('update_fetch_tasks'); + $queue = DrupalQueue::get('update_fetch_tasks', TRUE); $queue->createQueue(); }