diff --git a/modules/field/field.api.php b/modules/field/field.api.php index b83dc8db26aca138e33d0faebfad2bd361256613..24f5e583947c2766cb49ad8a64ff3108e0787eea 100644 --- a/modules/field/field.api.php +++ b/modules/field/field.api.php @@ -238,6 +238,10 @@ function hook_field_load($obj_type, $objects, $field, $instances, &$items, $age) * The type of $object. * @param $object * The object for the operation. + * Note that this might not be a full-fledged 'object'. When invoked through + * field_attach_query(), the $object will only include properties that the + * Field API knows about: bundle, id, revision id, and field values (no node + * title, user name...). * @param $field * The field structure for the operation. * @param $instance @@ -503,12 +507,17 @@ function hook_field_attach_pre_load($obj_type, $objects, $age, &$skip_fields) { * This hook is invoked after the field module has performed the operation. * * Unlike other field_attach hooks, this hook accounts for 'multiple loads'. - * It takes an array of objects indexed by object id as its first parameter. - * For performance reasons, information for all available objects should be - * loaded in a single query where possible. + * Instead of the usual $object parameter, it accepts an array of objects, + * indexed by object id. For performance reasons, information for all available + * objects should be loaded in a single query where possible. * - * Note that the changes made to the objects' field values get cached by the - * field cache for subsequent loads. + * Note that $objects might not be full-fledged 'objects'. When invoked through + * field_attach_query(), each object only includes properties that the Field + * API knows about: bundle, id, revision id, and field values (no node title, + * user name...) + + * The changes made to the objects' field values get cached by the field cache + * for subsequent loads. * * See field_attach_load() for details and arguments. */ @@ -585,6 +594,38 @@ function hook_field_attach_pre_insert($obj_type, $object, &$skip_fields) { function hook_field_attach_pre_update($obj_type, $object, &$skip_fields) { } +/** + * Act on field_attach_pre_query. + * + * This hook should be implemented by modules that use + * hook_field_attach_pre_load(), hook_field_attach_pre_insert() and + * hook_field_attach_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 $result_format + * See field_attach_query(). + * @param $age + * - FIELD_LOAD_CURRENT: query the most recent revisions for all + * objects. The results will be keyed by object type and object id. + * - FIELD_LOAD_REVISION: query all revisions. The results will be keyed by + * object type and object revision id. + * @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_attach_pre_query($field_name, $conditions, $result_format, $age, &$skip_field) { +} + /** * Act on field_attach_delete. * @@ -736,6 +777,26 @@ function hook_field_storage_delete($obj_type, $object) { function hook_field_storage_delete_revision($obj_type, $object) { } +/** + * Handle a 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 $result_format + * See field_attach_query(). + * @param $age + * See field_attach_query(). + * @return + * See field_attach_query(). + */ +function hook_field_storage_query($field_name, $conditions, $result_format, $age) { +} + /** * Act on creation of a new bundle. * diff --git a/modules/field/field.attach.inc b/modules/field/field.attach.inc index 4a24c171361519c1e0eda9fe34521fa14b3d3bc2..9b9e92a3fe99d66d1440f4fe61a11102e4052896 100644 --- a/modules/field/field.attach.inc +++ b/modules/field/field.attach.inc @@ -13,8 +13,7 @@ // Should all iteration through available fields be done here instead of in Field? /** - * Exception class thrown by field_attach_validate() when field - * validation errors occur. + * Exception thrown by field_attach_validate() on field validation errors. */ class FieldValidationException extends FieldException { var $errors; @@ -37,6 +36,15 @@ function __construct($errors) { } } +/** + * 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 * @{ @@ -146,47 +154,77 @@ function __construct($errors) { * @param $b * - The $form_state in the 'submit' operation. * - Otherwise NULL. - * - * @param $default - * - TRUE: use the default field implementation of the field hook. - * - FALSE: use the field module's implementation of the field hook. + * @param $options + * An associative array of additional options, with the following keys: + * - 'field_name' + * The name of the field whose operation should be invoked. By default, the + * operation is invoked on all the fields in the object's bundle. + * - 'default' + * A boolean value, specifying which implementation of the operation should + * be invoked. + * - if FALSE (default), the field types implementation of the operation + * will be invoked (hook_field_[op]) + * - If TRUE, the default field implementation of the field operation + * will be invoked (field_default_[op]) + * Internal use only. Do not explicitely set to TRUE, but use + * _field_invoke_default() instead. */ -function _field_invoke($op, $obj_type, $object, &$a = NULL, &$b = NULL, $default = FALSE) { - list(, , $bundle) = field_attach_extract_ids($obj_type, $object); - $instances = field_info_instances($bundle); - +function _field_invoke($op, $obj_type, $object, &$a = NULL, &$b = NULL, $options = array()) { + // Merge default options. + $default_options = array( + 'default' => FALSE, + ); + $options += $default_options; + + // Iterate through the object's field instances. $return = array(); - foreach ($instances as $instance) { + list(, , $bundle) = field_attach_extract_ids($obj_type, $object); + foreach (field_info_instances($bundle) as $instance) { $field_name = $instance['field_name']; - $field = field_info_field($field_name); - $items = isset($object->$field_name) ? $object->$field_name : array(); - $function = $default ? 'field_default_' . $op : $field['module'] . '_field_' . $op; - if (drupal_function_exists($function)) { - $result = $function($obj_type, $object, $field, $instance, $items, $a, $b); - if (is_array($result)) { - $return = array_merge($return, $result); + // When in 'single field' mode, only act on the specified field. + if (empty($options['field_name']) || $options['field_name'] == $field_name) { + $field = field_info_field($field_name); + + // Extract the field values into a separate variable, easily accessed by + // hook implementations. + $items = isset($object->$field_name) ? $object->$field_name : array(); + + // Invoke the field hook and collect results. + $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; + if (drupal_function_exists($function)) { + $result = $function($obj_type, $object, $field, $instance, $items, $a, $b); + if (isset($result)) { + // For hooks with array results, we merge results together. + // For hooks with scalar results, we collect results in an array. + if (is_array($result)) { + $return = array_merge($return, $result); + } + else { + $return[] = $result; + } + } } - elseif (isset($result)) { - $return[] = $result; + + // Populate field values back in the object, but avoid replacing missing + // fields with an empty array (those are not equivalent on update). + if ($items !== array() || property_exists($object, $field_name)) { + $object->$field_name = $items; } } - // Populate field values back in the object, but avoid replacing missing - // fields with an empty array (those are not equivalent on update). - if ($items !== array() || property_exists($object, $field_name)) { - $object->$field_name = $items; - } } return $return; } /** - * Invoke a field operation across fields on multiple objects. + * Invoke a field hook across fields on multiple objects. * * @param $op * Possible operations include: * - load + * For all other operations, use _field_invoke() / field_invoke_default() + * instead. * @param $obj_type * The type of $object; e.g. 'node' or 'user'. * @param $objects @@ -196,53 +234,75 @@ function _field_invoke($op, $obj_type, $object, &$a = NULL, &$b = NULL, $default * - Otherwise NULL. * @param $b * Currently always NULL. - * @param $default - * - TRUE: use the default field implementation of the field hook. - * - FALSE: use the field module's implementation of the field hook. + * @param $options + * An associative array of additional options, with the following keys: + * - 'field_name' + * The name of the field whose operation should be invoked. By default, the + * operation is invoked on all the fields in the objects' bundles. + * - 'default' + * A boolean value, specifying which implementation of the operation should + * be invoked. + * - if FALSE (default), the field types implementation of the operation + * will be invoked (hook_field_[op]) + * - If TRUE, the default field implementation of the field operation + * will be invoked (field_default_[op]) + * Internal use only. Do not explicitely set to TRUE, but use + * _field_invoke_multiple_default() instead. * @return * An array of returned values keyed by object id. */ -function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL, $default = FALSE) { +function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL, $options = array()) { + // Merge default options. + $default_options = array( + 'default' => FALSE, + ); + $options += $default_options; + $fields = array(); $grouped_instances = array(); $grouped_objects = array(); $grouped_items = array(); $return = array(); - // Preparation: - // - Get the list of fields contained in the various bundles. - // - For each field, group the corresponding instances, objects and field - // values. - // - Initialize the return value for each object. + // Go through the objects and collect the fields on which the hook should be + // invoked. foreach ($objects as $object) { list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); foreach (field_info_instances($bundle) as $instance) { $field_name = $instance['field_name']; - if (!isset($grouped_fields[$field_name])) { - $fields[$field_name] = field_info_field($field_name); + // When in 'single field' mode, only act on the specified field. + if (empty($options['field_name']) || $options['field_name'] == $field_name) { + // Add the field to the list of fields to invoke the hook on. + if (!isset($fields[$field_name])) { + $fields[$field_name] = field_info_field($field_name); + } + // Group the corresponding instances and objects. + $grouped_instances[$field_name][$id] = $instance; + $grouped_objects[$field_name][$id] = $objects[$id]; + // Extract the field values into a separate variable, easily accessed + // by hook implementations. + $grouped_items[$field_name][$id] = isset($object->$field_name) ? $object->$field_name : array(); } - $grouped_instances[$field_name][$id] = $instance; - $grouped_objects[$field_name][$id] = $objects[$id]; - $grouped_items[$field_name][$id] = isset($object->$field_name) ? $object->$field_name : array(); } + // Initialize the return value for each object. $return[$id] = array(); } - // Call each field's operation. + // For each field, invoke the field hook and collect results. foreach ($fields as $field_name => $field) { - if (!empty($field)) { - $function = $default ? 'field_default_' . $op : $field['module'] . '_field_' . $op; - if (drupal_function_exists($function)) { - $results = $function($obj_type, $grouped_objects[$field_name], $field, $grouped_instances[$field_name], $grouped_items[$field_name], $a, $b); - // Merge results by object. - if (isset($results)) { - foreach ($results as $id => $result) { - if (is_array($result)) { - $return[$id] = array_merge($return[$id], $result); - } - else { - $return[$id][] = $result; - } + $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; + if (drupal_function_exists($function)) { + $results = $function($obj_type, $grouped_objects[$field_name], $field, $grouped_instances[$field_name], $grouped_items[$field_name], $a, $b); + if (isset($results)) { + // Collect results by object. + // For hooks with array results, we merge results together. + // For hooks with scalar results, we collect results in an array. + foreach ($results as $id => $result) { + if (is_array($result)) { + $return[$id] = array_merge($return[$id], $result); + } + else { + $return[$id][] = $result; } } } @@ -262,16 +322,30 @@ function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL /** * Invoke field.module's version of a field hook. + * + * This function invokes the field_default_[op]() function. + * Use _field_invoke() to invoke the field type implementation, + * hook_field_[op](). + * + * @see _field_invoke(). */ -function _field_invoke_default($op, $obj_type, $object, &$a = NULL, &$b = NULL) { - return _field_invoke($op, $obj_type, $object, $a, $b, TRUE); +function _field_invoke_default($op, $obj_type, $object, &$a = NULL, &$b = NULL, $options = array()) { + $options['default'] = TRUE; + return _field_invoke($op, $obj_type, $object, $a, $b, $options); } /** * Invoke field.module's version of a field hook on multiple objects. + * + * This function invokes the field_default_[op]() function. + * Use _field_invoke_multiple() to invoke the field type implementation, + * hook_field_[op](). + * + * @see _field_invoke_multiple(). */ -function _field_invoke_multiple_default($op, $obj_type, $objects, &$a = NULL, &$b = NULL) { - return _field_invoke_multiple($op, $obj_type, $objects, $a, $b, TRUE); +function _field_invoke_multiple_default($op, $obj_type, $objects, &$a = NULL, &$b = NULL, $options = array()) { + $options['default'] = TRUE; + return _field_invoke_multiple($op, $obj_type, $objects, $a, $b, $options); } /** @@ -651,6 +725,154 @@ function field_attach_delete_revision($obj_type, $object) { } } +/** + * Retrieve objects 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 object. + * + * @param $field_name + * The name 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 for $field_name's field type: condition on field + * value, + * - 'type': condition on object type (e.g. 'node', 'user'...), + * - 'bundle': condition on object bundle (e.g. node type), + * - 'entity_id': condition on object id (e.g node nid, user uid...), + * The field_attach_query_revisions() function additionally supports: + * - 'revision_id': condition on object 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': 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 $result_format + * - FIELD_QUERY_RETURN_IDS (default): return the ids of the objects matching the + * conditions. + * - FIELD_QUERY_RETURN_VALUES: return the values for the field. + * @param $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 + * objects. The results will be keyed by object type and object id. + * - FIELD_LOAD_REVISION: query all revisions. The results will be keyed by + * object type and object revision id. + * @return + * An array keyed by object type (e.g. 'node', 'user'...), then by object id + * or revision id (depending of the value of the $age parameter), and whose + * values depend on the $result_format parameter: + * - FIELD_QUERY_RETURN_IDS: the object id. + * - FIELD_QUERY_RETURN_VALUES: a pseudo-object with values for the + * $field_name field. This only includes values matching the conditions, + * and thus might not contain all actual values and actual delta sequence + * (although values oprder is preserved). + * The pseudo-objects only include properties that the Field API knows + * about: bundle, id, revision id, and field values (no node title, user + * name...). + * Throws a FieldQueryException if the field's storage doesn't support the + * specified conditions. + */ +function field_attach_query($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_IDS, $age = FIELD_LOAD_CURRENT) { + // Give a chance to 3rd party modules that bypass the storage engine to + // handle the query. + $skip_field = FALSE; + foreach (module_implements('field_attach_pre_query') as $module) { + $function = $module . '_field_attach_pre_query'; + $results = $function($field_name, $conditions, $result_format, $age, $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) { + $results = module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_query', $field_name, $conditions, $result_format, $age); + } + + if ($result_format == FIELD_QUERY_RETURN_VALUES) { + foreach ($results as $obj_type => $pseudo_objects) { + if ($age == FIELD_LOAD_CURRENT) { + // Invoke hook_field_load(). + $b = NULL; + _field_invoke_multiple('load', $obj_type, $pseudo_objects, $age, $b, array('field_name' => $field_name)); + + // Invoke hook_field_attach_load(). + foreach (module_implements('field_attach_load') as $module) { + $function = $module . '_field_attach_load'; + $function($obj_type, $pseudo_objects, $age); + } + } + else { + // The 'multiple' hooks expect an array of objects keyed by object id, + // and thus cannot be used directly when querying revisions. The hooks + // are therefore called on each object separately, which might cause + // performance issues when large numbers of revisions are retrieved. + foreach ($pseudo_objects as $vid => $pseudo_object) { + list($id) = _field_attach_extract_ids($obj_type, $pseudo_object); + $objects = array($id => $pseudo_object); + + // Invoke hook_field_load(). + $b = NULL; + _field_invoke_multiple('load', $obj_type, $objects, $age, $b, array('field_name' => $field_name)); + + // Invoke hook_field_attach_load(). + foreach (module_implements('field_attach_load') as $module) { + $function = $module . '_field_attach_load'; + $function($obj_type, $objects, $age); + } + } + } + } + } + + return $results; +} + +/** + * Retrieve object revisions matching a given set of conditions. + * + * See field_attach_query() for more informations. + * + * @param $field_name + * The name of the field to query. + * @param $conditions + * See field_attach_query(). + * @param $result_format + * See field_attach_query(). + * Note that the FIELD_QUERY_RETURN_VALUES option might cause performance + * issues with field_attach_query_revisions(). + * @return + * See field_attach_query(). + */ +function field_attach_query_revisions($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_IDS) { + return field_attach_query($field_name, $conditions, $result_format, FIELD_LOAD_REVISION); +} + /** * Generate and return a structured content array tree suitable for * drupal_render() for all of the fields on an object. The format of @@ -811,6 +1033,35 @@ function field_attach_extract_ids($object_type, $object) { return array($id, $vid, $bundle, $cacheable); } +/** + * Helper function to assemble an object structure with initial ids. + * + * This function can be seen as reciprocal to field_attach_extract_ids(). + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $ids + * A numerically indexed array, as returned by field_attach_extract_ids(), + * containing these elements: + * 0: primary id of the object + * 1: revision id of the object, or NULL if $obj_type is not versioned + * 2: bundle name of the object + * @return + * An $object structure, initialized with the ids provided. + */ +function field_attach_create_stub_object($obj_type, $ids) { + $object = new stdClass(); + $info = field_info_fieldable_types($obj_type); + $object->{$info['id key']} = $ids[0]; + if (isset($info['revision key']) && !is_null($ids[1])) { + $object->{$info['revision key']} = $ids[1]; + } + if ($info['bundle key']) { + $object->{$info['bundle key']} = $ids[2]; + } + return $object; +} + /** * @} End of "defgroup field_attach" */ diff --git a/modules/field/field.module b/modules/field/field.module index 29bf758faec47d35509c340081569406e173f613..f518169f975d9123600ff32c45e8936f367fc78a 100644 --- a/modules/field/field.module +++ b/modules/field/field.module @@ -87,6 +87,25 @@ */ define('FIELD_LOAD_REVISION', 'FIELD_LOAD_REVISION'); +/** + * @name Field query flags + * @{ + * Flags for use in the $result_format parameter in field_attach_query(). + */ + +/** + * Result format argument for field_attach_query(). + */ +define('FIELD_QUERY_RETURN_VALUES', 'FIELD_QUERY_RETURN_VALUES'); + +/** + * Result format argument for field_attach_query(). + */ +define('FIELD_QUERY_RETURN_IDS', 'FIELD_QUERY_RETURN_IDS'); + +/** + * @} End of "Field query flags". + */ /** * Base class for all exceptions thrown by Field API functions. diff --git a/modules/field/field.test b/modules/field/field.test index c67eb72ab29b4b84c7cc053942940b33d77823f1..826410bd32575ed8d60fff5b48f3f926f58f5d74 100644 --- a/modules/field/field.test +++ b/modules/field/field.test @@ -261,6 +261,216 @@ class FieldAttachTestCase extends DrupalWebTestCase { $this->assertEqual($entity->{$this->field_name}, $values, t('Insert: missing field results in default value saved')); } + /** + * Test field_attach_query(). + */ + function testFieldAttachQuery() { + $cardinality = $this->field['cardinality']; + + // 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 two test objects, 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 object 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}[$delta] = array('value' => $values[$delta]); + } + field_attach_insert($entity_types[1], $entities[1]); + + // Create second test object, sharing a value with the first one. + $common_value = $values[$cardinality - 1]; + $entities[2]->{$this->field_name} = array(array('value' => $common_value)); + field_attach_insert($entity_types[2], $entities[2]); + + // Query on the object's values. + for ($delta = 0; $delta < $cardinality; $delta++) { + $conditions = array(array('value', $values[$delta])); + $result = field_attach_query($this->field_name, $conditions); + $this->assertTrue(isset($result[$entity_types[1]][1]), t('Query on value %delta returns the object', array('%delta' => $delta))); + } + + // Query on a value that is not in the object. + 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_name, $conditions); + $this->assertFalse(isset($result[$entity_types[1]][1]), t("Query on a value that is not in the object doesn't return the object")); + + // Query on the value shared by both objects, and discriminate using + // additional conditions. + + $conditions = array(array('value', $common_value)); + $result = field_attach_query($this->field_name, $conditions); + $this->assertTrue(isset($result[$entity_types[1]][1]) && isset($result[$entity_types[2]][2]), t('Query on a value common to both objects returns both objects')); + + $conditions = array(array('type', $entity_types[1]), array('value', $common_value)); + $result = field_attach_query($this->field_name, $conditions); + $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both objects and a 'type' condition only returns the relevant object")); + + $conditions = array(array('bundle', $entities[1]->fttype), array('value', $common_value)); + $result = field_attach_query($this->field_name, $conditions); + $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both objects and a 'bundle' condition only returns the relevant object")); + + $conditions = array(array('entity_id', $entities[1]->ftid), array('value', $common_value)); + $result = field_attach_query($this->field_name, $conditions); + $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both objects and an 'entity_id' condition only returns the relevant object")); + + // Test FIELD_QUERY_RETURN_IDS result format. + $conditions = array(array('value', $values[0])); + $result = field_attach_query($this->field_name, $conditions); + $expected = array( + $entity_types[1] => array( + $entities[1]->ftid => $entities[1]->ftid, + ) + ); + $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_IDS result format returns the expect result')); + + // Test FIELD_QUERY_RETURN_VALUES result format. + // Configure the instances so that we test hook_field_load() (see + // field_test_field_load() in field_test.module). + $this->instance['settings']['test_hook_field_load'] = TRUE; + field_update_instance($this->instance); + $this->instance2['settings']['test_hook_field_load'] = TRUE; + field_update_instance($this->instance2); + + // Query for one of the values in the 1st object and the value shared by + // both objects. + $conditions = array(array('value', array($values[0], $common_value))); + $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_RETURN_VALUES); + $expected = array( + $entity_types[1] => array( + $entities[1]->ftid => (object) array( + 'ftid' => $entities[1]->ftid, + 'ftvid' => $entities[1]->ftvid, + 'fttype' => $entities[1]->fttype, + $this->field_name => array( + array('value' => $values[0], 'additional_key' => 'additional_value'), + array('value' => $common_value, 'additional_key' => 'additional_value'), + ), + ), + ), + $entity_types[2] => array( + $entities[2]->ftid => (object) array( + 'ftid' => $entities[2]->ftid, + 'ftvid' => $entities[2]->ftvid, + 'fttype' => $entities[2]->fttype, + $this->field_name => array( + array('value' => $common_value, 'additional_key' => 'additional_value'), + ), + ), + ), + ); + $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_VALUES result format returns the expect result')); + } + + /** + * Test field_attach_query_revisions(). + */ + function testFieldAttachQueryRevisions() { + $cardinality = $this->field['cardinality']; + + // Create first object 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)); + $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}[$delta] = array('value' => $values[$delta]); + } + field_attach_insert($entity_type, $entities[1]); + + // Create second object revision, sharing a value with the first one. + $common_value = $values[$cardinality - 1]; + $entities[2]->{$this->field_name}[0] = array('value' => $common_value); + field_attach_update($entity_type, $entities[2]); + + // Query on the object's values. + for ($delta = 0; $delta < $cardinality; $delta++) { + $conditions = array(array('value', $values[$delta])); + $result = field_attach_query_revisions($this->field_name, $conditions); + $this->assertTrue(isset($result[$entity_type][1]), t('Query on value %delta returns the object', array('%delta' => $delta))); + } + + // Query on a value that is not in the object. + 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_name, $conditions); + $this->assertFalse(isset($result[$entity_type][1]), t("Query on a value that is not in the object doesn't return the object")); + + // Query on the value shared by both objects, and discriminate using + // additional conditions. + + $conditions = array(array('value', $common_value)); + $result = field_attach_query_revisions($this->field_name, $conditions); + $this->assertTrue(isset($result[$entity_type][1]) && isset($result[$entity_type][2]), t('Query on a value common to both objects returns both objects')); + + $conditions = array(array('revision_id', $entities[1]->ftvid), array('value', $common_value)); + $result = field_attach_query_revisions($this->field_name, $conditions); + $this->assertTrue(isset($result[$entity_type][1]) && !isset($result[$entity_type][2]), t("Query on a value common to both objects and a 'revision_id' condition only returns the relevant object")); + + // Test FIELD_QUERY_RETURN_IDS result format. + $conditions = array(array('value', $values[0])); + $result = field_attach_query_revisions($this->field_name, $conditions); + $expected = array( + $entity_type => array( + $entities[1]->ftvid => $entities[1]->ftid, + ) + ); + $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_IDS result format returns the expect result')); + + // Test FIELD_QUERY_RETURN_VALUES result format. + // Configure the instance so that we test hook_field_load() (see + // field_test_field_load() in field_test.module). + $this->instance['settings']['test_hook_field_load'] = TRUE; + field_update_instance($this->instance); + + // Query for one of the values in the 1st object and the value shared by + // both objects. + $conditions = array(array('value', array($values[0], $common_value))); + $result = field_attach_query_revisions($this->field_name, $conditions, FIELD_QUERY_RETURN_VALUES); + $expected = array( + $entity_type => array( + $entities[1]->ftvid => (object) array( + 'ftid' => $entities[1]->ftid, + 'ftvid' => $entities[1]->ftvid, + 'fttype' => $entities[1]->fttype, + $this->field_name => array( + array('value' => $values[0], 'additional_key' => 'additional_value'), + array('value' => $common_value, 'additional_key' => 'additional_value'), + ), + ), + $entities[2]->ftvid => (object) array( + 'ftid' => $entities[2]->ftid, + 'ftvid' => $entities[2]->ftvid, + 'fttype' => $entities[2]->fttype, + $this->field_name => array( + array('value' => $common_value, 'additional_key' => 'additional_value'), + ), + ), + ), + ); + $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_VALUES result format returns the expect result')); + + // TODO : test + } + function testFieldAttachViewAndPreprocess() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.module b/modules/field/modules/field_sql_storage/field_sql_storage.module index 18f89adb83b482d8511062811b35b1cd255ff169..5a3d659b9779193b2f3d47457a3e5eeba7000b51 100644 --- a/modules/field/modules/field_sql_storage/field_sql_storage.module +++ b/modules/field/modules/field_sql_storage/field_sql_storage.module @@ -238,7 +238,8 @@ function field_sql_storage_field_storage_load($obj_type, &$objects, $age, $skip_ // For each column declared by the field, populate the item // from the prefixed database column. foreach ($field['columns'] as $column => $attributes) { - $item[$column] = $row->{_field_sql_storage_columnname($field_name, $column)}; + $column_name = _field_sql_storage_columnname($field_name, $column); + $item[$column] = $row->$column_name; } // Add the item to the field values for the entity. @@ -350,6 +351,108 @@ function field_sql_storage_field_storage_delete($obj_type, $object) { } } + +/** + * Implement hook_field_storage_query(). + */ +function field_sql_storage_field_storage_query($field_name, $conditions, $result_format, $age) { + $load_values = $result_format == FIELD_QUERY_RETURN_VALUES; + $load_current = $age == FIELD_LOAD_CURRENT; + + $field = field_info_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'); + $query + ->fields('e', array('type')) + ->condition('deleted', 0) + ->orderBy('delta'); + // Add fields, depending on the return format. + if ($load_values) { + $query->fields('t'); + } + else { + $query->fields('t', array('entity_id', 'revision_id')); + } + // 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 NULL: + $operator = is_array($value) ? 'IN' : '='; + break; + + case 'STARTS_WITH': + $operator = 'LIKE'; + $value .= '%'; + break; + + case 'ENDS_WITH': + $operator = 'LIKE'; + $value = "$value%"; + break; + + case 'CONTAINS': + $operator = 'LIKE'; + $value = "%$value%"; + break; + } + // Translate field columns into prefixed db columns. + if (in_array($column, $field_columns)) { + $column = _field_sql_storage_columnname($field_name, $column); + } + $query->condition($column, $value, $operator); + } + + $results = $query->execute(); + + // Build results. + $return = array(); + $delta_count = array(); + foreach ($results as $row) { + // If querying all revisions and the entity type has revisions, we need to + // key the results by revision_ids. + $entity_type = field_info_fieldable_types($row->type); + $id = ($load_current || empty($entity_type['revision key'])) ? $row->entity_id : $row->revision_id; + + if ($load_values) { + // Populate actual field values. + if (!isset($delta_count[$row->type][$id])) { + $delta_count[$row->type][$id] = 0; + } + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->type][$id] < $field['cardinality']) { + $item = array(); + // For each column declared by the field, populate the item + // from the prefixed database column. + foreach ($field['columns'] as $column => $attributes) { + $column_name = _field_sql_storage_columnname($field_name, $column); + $item[$column] = $row->$column_name; + } + + // Initialize the 'pseudo object' if needed. + if (!isset($return[$row->type][$id])) { + $return[$row->type][$id] = field_attach_create_stub_object($row->type, array($row->entity_id, $row->revision_id, $row->bundle)); + } + // Add the item to the field values for the entity. + $return[$row->type][$id]->{$field_name}[] = $item; + $delta_count[$row->type][$id]++; + } + } + else { + // Simply return the list of selected ids. + $return[$row->type][$id] = $row->entity_id; + } + } + + return $return; +} + /** * Implement hook_field_storage_delete_revision(). *