=== 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-04-29 07:31:50 +0000 @@ -1044,108 +1044,240 @@ 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. + * Storage engines are not required to support all kinds of queries. A + * FieldQueryException 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. */ -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; +class FieldAttachQuery { + protected $fieldConditions = array(); + protected $entityConditions = array(); + protected $entityTypes = array(); + protected $fieldOrder = array(); + protected $entityOrder = array(); + protected $range = array(); + protected $deleted = FALSE; + protected $storage = NULL; + + /** + * 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 + * 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 fieldAttachQuery + * The called object. + */ + public function fieldCondition($field_name, $column, $value, $operator = NULL, $delta_group = NULL, $language_group = NULL) { + $this->findStorage($field_name); + $this->fieldConditions[] = array( + 'field_name' => $field_name, + 'column' => $column, + 'value' => $value, + 'operator' => $operator, + 'delta_group' => $delta_group, + 'language_group' => $language_group, + ); + } + + /** + * 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 fieldAttachQuery + * 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 FieldAttachQuery + * The called object. + */ + public function entityOrderBy($column, $direction) { + $this->entityOrder[] = array( + 'column' => $column, + 'direction' => $direction, + ); + } + public function fieldOrderBy($field_name, $column, $direction) { + $this->findStorage($field_name); + $this->fieldOrder[] = array( + 'field_name' => $field_name, + 'column' => $column, + 'direction' => $direction, + ); + } + /** + * 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 fieldAttachQuery + * 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. + */ + public function deleted($deleted) { + $this->deleted = $deleted; + } - // 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); + public function execute() { + foreach (module_implements('field_storage_pre_query') as $module) { + $function = $module . '_field_storage_pre_query'; + $result = $function($this->entityTypes, $this->entityConditions, $this->fieldConditions, $this->fieldOrder, $this->entityOrder, $this->range, $this->deleted); + if ($result) { + return $result; + } + } + if (isset($this->storage)) { + $function = $this->storage['module'] . '_field_storage_query'; + return $function($this->entityTypes, $this->entityConditions, $this->fieldConditions, $this->fieldOrder, $this->entityOrder, $this->range, $this->deleted); + } + return $this->entityQuery(); } - return $results; + 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 FieldQueryException(t("Can't query multiple entity tables at once.")); + } + else { + throw new FieldQueryException(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 FieldQueryException(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']); + } + return $query->execute(); + } + + protected function findStorage($field_name) { + $field = field_info_field($field_name); + $storage = $field['storage']; + if (!isset($this->storage)) { + $this->storage = $storage; + } + elseif (array_diff($this->storage, $field['storage'])) { + throw new FieldQueryException(t("Can't handle more than one field storage engine")); + } + } } /** === 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-04-29 06:58:01 +0000 @@ -472,129 +472,87 @@ 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) { + $delta_groups = array(); + foreach ($field_conditions as $key => $condition) { + $field = field_info_field($condition['field_name']); + $tablename = _field_sql_storage_tablename($field); + if (isset($query)) { + $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'); + $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']); + $query->condition("$table_alias.$columnname", $condition['value'], $condition['operator']); + if (isset($condition['delta_group'])) { + $delta_group = $condition['delta_group']; + if (!isset($delta_groups[$delta_group])) { + $delta_groups[$delta_group] = $table_alias; } else { - $value = _field_sql_storage_etid($value); + $query->where("$table_alias.delta = " . $delta_groups[$delta_group] . '.delta'); } } - // 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 FieldQueryException(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; + } + 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 = $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'], $options['limit']); + } + if (isset($deleted)) { + $query->condition("$table_alias.deleted", $deleted); + } + return $query->execute(); +} - return $return; +function _field_sql_storage_query_join_entity($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; + 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/tests/field.test' --- modules/field/tests/field.test 2010-04-11 18:33:43 +0000 +++ modules/field/tests/field.test 2010-04-29 06:57:38 +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')); - } } /** @@ -3108,3 +2899,282 @@ class FieldBulkDeleteTestCase extends Fi $this->assertEqual(count($fields), 0, 'The field is purged.'); } } + +class FieldAttachQueryTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Field attach query', + 'description' => 'Test field_attach_query.', + 'group' => 'Field 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 testFieldAttachQuery() { + // First, test without options. + $query = new fieldAttachQuery(); + $query->fieldCondition($this->fields[0], 'value', 3, '='); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 3), + ), t('Test the "equal to" operation.')); + $query = new fieldAttachQuery(); + $query->fieldCondition($this->fields[0], 'value', 2, '>'); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Test the "greater than" operation.')); + + $query = new fieldAttachQuery(); + $query->fieldCondition($this->fields[0], 'value', array(3, 4), 'IN'); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Test the "in" operation.')); + + $query = new fieldAttachQuery(); + $query->fieldCondition($this->fields[0], 'value', array(1, 3), 'BETWEEN'); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 1), + array('test_entity_1', 2), + array('test_entity_1', 3), + ), t('Test the "between" operation.')); + + $query = new fieldAttachQuery(); + $query->fieldCondition($this->fields[0], 'value', 3); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 3), + ), t('Test omission of an operator with a single item.')); + + $query = new fieldAttachQuery(); + $query->fieldCondition($this->fields[0], 'value', array(3, 4)); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Test omission of an operator with multiple items.')); + + $query = new fieldAttachQuery(); + $query->entityCondition('test_entity_1', 'ftid', 1); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 1), + ), t('Test entity conditions.')); + + $query = new fieldAttachQuery(); + $query->entityCondition('test_entity_1', 'ftid', 1, '>'); + $query->fieldCondition($this->fields[0], 'value', 4, '<'); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 2), + array('test_entity_1', 3), + ), t('Test entity and field conditions.')); + + $query = new fieldAttachQuery(); + $query->fieldCondition($this->fields[0], 'value', 0, '>'); + $query->fieldOrderBy($this->fields[0], 'value', 'asc'); + $this->_testFieldAttachQuery($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 fieldAttachQuery(); + $query->fieldCondition($this->fields[0], 'value', 0, '>'); + $query->fieldOrderBy($this->fields[0], 'value', 'desc'); + $this->_testFieldAttachQuery($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 fieldAttachQuery(); + $query->entityTypes(array('test_entity_1')); + $query->entityOrderBy('ftid', 'asc'); + $this->_testFieldAttachQuery($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 fieldAttachQuery(); + $query->entityTypes(array('test_entity_1')); + $query->entityOrderBy('ftid', 'desc'); + $this->_testFieldAttachQuery($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 fieldAttachQuery(); + $query->entityTypes(array('test_entity_1')); + $query->entityOrderBy('ftid', 'asc'); + $query->range(0, 2); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 1), + array('test_entity_1', 2), + ), t('Test limit.'), TRUE); + + $query = new fieldAttachQuery(); + $query->entityTypes(array('test_entity_1')); + $query->entityOrderBy('ftid', 'asc'); + $query->range(2, 4); + $this->_testFieldAttachQuery($query, array( + array('test_entity_1', 3), + array('test_entity_1', 4), + ), t('Test offset.'), TRUE); + + for ($i = 5; $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 fieldAttachQuery(); + $query->fieldCondition($this->fields[0], 'value', 2, '>'); + $this->_testFieldAttachQuery($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 fieldAttachQuery(); + $query->fieldCondition($this->fields[1], 'shape', 'square'); + $query->fieldCondition($this->fields[1], 'color', 'blue'); + $this->_testFieldAttachQuery($query, array( + array('test_entity_2', 5), + ), t('Test without a delta group.')); + + $query = new fieldAttachQuery(); + $query->fieldCondition($this->fields[1], 'shape', 'square', '=', 'group'); + $query->fieldCondition($this->fields[1], 'color', 'blue', '=', 'group'); + $this->_testFieldAttachQuery($query, array(), t('Test with a delta group.')); + + $pass = FALSE; + $query = new fieldAttachQuery(); + try { + $query->execute(); + } + catch (FieldQueryException $exception) { + $pass = ($exception->getMessage() == t('Empty query')); + } + $this->assertTrue($pass, t("Can't query the universe.")); + } + + function _testFieldAttachQuery($query, $intended_results, $assertion, $sort = FALSE) { + $results = array(); + foreach ($query->execute() as $entity) { + if (!is_object($entity)) { + $entity = (object)$entity; + } + // These should be the test entities where that field is 2 or more. + $results[] = array($entity->entity_type, $entity->entity_id); + } + if (!isset($sort) || !$sort) { + sort($results); + sort($intended_results); + } + $this->assertEqual($results, $intended_results, $assertion); + } +} === 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-04-25 19:12:42 +0000 @@ -46,6 +46,27 @@ function field_test_entity_info() { 'bundles' => $bundles, 'view modes' => $test_entity_modes, ), + 'test_entity_1' => array( + 'name' => t('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'), + '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-04-25 18:54:44 +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, + ), + ), + ); + } } /** === 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-04-25 06:05:12 +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-04-23 05:46:01 +0000 +++ modules/simpletest/drupal_web_test_case.php 2010-04-29 06:42:46 +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.