summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDries Buytaert2009-09-27 12:52:55 (GMT)
committerDries Buytaert2009-09-27 12:52:55 (GMT)
commit803b8b3940257330e3945e243d559718c85cc481 (patch)
treeba082ac801da31f06385ead5bea4338eb4ccc4c0
parent252996066042b290bff9bebba8f8256cab62a999 (diff)
- Patch #443422 by yched, bjaspan | chx, merlinofchaos, Scott Reynolds, plach, profix898, mattyoung: added support for pluggable 'per field' storage engine. Comes with documentation and tests.
The Field Attach API uses the Field Storage API to perform all "database access". Each Field Storage API hook function defines a primitive database operation such as read, write, or delete. The default field storage module, field_sql_storage.module, uses the local SQL database to implement these operations, but alternative field storage backends can choose to represent the data in SQL differently or use a completely different storage mechanism such as a cloud-based database.
-rw-r--r--modules/field/field.api.php109
-rw-r--r--modules/field/field.attach.inc160
-rw-r--r--modules/field/field.crud.inc129
-rw-r--r--modules/field/field.info.inc73
-rw-r--r--modules/field/field.install70
-rw-r--r--modules/field/field.module47
-rw-r--r--modules/field/field.test183
-rw-r--r--modules/field/modules/field_sql_storage/field_sql_storage.install4
-rw-r--r--modules/field/modules/field_sql_storage/field_sql_storage.module222
-rw-r--r--modules/field/modules/field_sql_storage/field_sql_storage.test8
-rw-r--r--modules/simpletest/tests/field_test.module440
11 files changed, 1140 insertions, 305 deletions
diff --git a/modules/field/field.api.php b/modules/field/field.api.php
index 6fd2cef..380444a 100644
--- a/modules/field/field.api.php
+++ b/modules/field/field.api.php
@@ -70,10 +70,10 @@ function hook_field_extra_fields($bundle) {
/**
* @defgroup field_types Field Types API
* @{
- * Define field types, widget types, and display formatter types.
+ * Define field types, widget types, display formatter types, storage types.
*
* The bulk of the Field Types API are related to field types. A field type
- * represents a particular data storage type (integer, string, date, etc.) that
+ * represents a particular type of data (integer, string, date, etc.) that
* can be attached to a fieldable object. hook_field_info() defines the basic
* properties of a field type, and a variety of other field hooks are called by
* the Field Attach API to perform field-type-specific actions.
@@ -97,6 +97,9 @@ function hook_field_extra_fields($bundle) {
* behavior of existing field types.
* @see hook_field_widget_info().
* @see hook_field_formatter_info().
+ *
+ * A third kind of pluggable handlers, storage backends, is defined by the
+ * @link field_storage Field Storage API @endlink.
*/
/**
@@ -1097,6 +1100,45 @@ function hook_field_attach_delete_bundle($bundle, $instances) {
*/
/**
+ * Expose Field API storage backends.
+ *
+ * @return
+ * An array describing the storage backends implemented by the module.
+ * The keys are storage backend names. To avoid name clashes, storage backend
+ * names should be prefixed with the name of the module that exposes them.
+ * The values are arrays describing the storage backend, with the following
+ * key/value pairs:
+ * - label: The human-readable name of the storage backend.
+ * - description: A short description for the storage backend.
+ * - settings: An array whose keys are the names of the settings available
+ * for the storage backend, and whose values are the default values for
+ * those settings.
+ */
+function hook_field_storage_info() {
+ return array(
+ 'field_sql_storage' => array(
+ 'label' => t('Default SQL storage'),
+ 'description' => t('Stores fields in the local SQL database, using per-field tables.'),
+ 'settings' => array(),
+ ),
+ );
+}
+
+/**
+ * Perform alterations on Field API storage types.
+ *
+ * @param $info
+ * Array of informations on storage types exposed by
+ * hook_field_field_storage_info() implementations.
+ */
+function hook_field_storage_info_alter(&$info) {
+ // Add a setting to a storage type.
+ $info['field_sql_storage']['settings'] += array(
+ 'mymodule_additional_setting' => 'default value',
+ );
+}
+
+/**
* Load field data for a set of objects.
*
* @param $obj_type
@@ -1107,15 +1149,15 @@ function hook_field_attach_delete_bundle($bundle, $instances) {
* FIELD_LOAD_CURRENT to load the most recent revision for all
* fields, or FIELD_LOAD_REVISION to load the version indicated by
* each object.
- * @param $skip_fields
- * An array keyed by field ids whose data has already been loaded and
- * therefore should not be loaded again. The values associated to these keys
- * are not specified.
+ * @param $fields
+ * An array listing the fields to be loaded. The keys of the array are field
+ * ids, the values of the array are the object ids (or revision ids,
+ * depending on the $age parameter) to be loaded for each field.
* @return
* Loaded field values are added to $objects. Fields with no values should be
* set as an empty array.
*/
-function hook_field_storage_load($obj_type, $objects, $age, $skip_fields) {
+function hook_field_storage_load($obj_type, $objects, $age, $fields) {
}
/**
@@ -1128,12 +1170,11 @@ function hook_field_storage_load($obj_type, $objects, $age, $skip_fields) {
* @param $op
* FIELD_STORAGE_UPDATE when updating an existing object,
* FIELD_STORAGE_INSERT when inserting a new object.
- * @param $skip_fields
- * An array keyed by field ids whose data has already been written and
- * therefore should not be written again. The values associated to these keys
- * are not specified.
+ * @param $fields
+ * An array listing the fields to be written. The keys and values of the
+ * array are field ids.
*/
-function hook_field_storage_write($obj_type, $object, $op, $skip_fields) {
+function hook_field_storage_write($obj_type, $object, $op, $fields) {
}
/**
@@ -1143,8 +1184,11 @@ function hook_field_storage_write($obj_type, $object, $op, $skip_fields) {
* The entity type of object, such as 'node' or 'user'.
* @param $object
* The object on which to operate.
+ * @param $fields
+ * An array listing the fields to delete. The keys and values of the
+ * array are field ids.
*/
-function hook_field_storage_delete($obj_type, $object) {
+function hook_field_storage_delete($obj_type, $object, $fields) {
}
/**
@@ -1159,8 +1203,11 @@ function hook_field_storage_delete($obj_type, $object) {
* The object on which to operate. The revision to delete is
* indicated by the object's revision id property, as identified by
* hook_fieldable_info() for $obj_type.
+ * @param $fields
+ * An array listing the fields to delete. The keys and values of the
+ * array are field ids.
*/
-function hook_field_storage_delete_revision($obj_type, $object) {
+function hook_field_storage_delete_revision($obj_type, $object, $fields) {
}
/**
@@ -1186,26 +1233,6 @@ function hook_field_storage_query($field_name, $conditions, $count, &$cursor = N
}
/**
- * Act on creation of a new bundle.
- *
- * @param $bundle
- * The name of the bundle being created.
- */
-function hook_field_storage_create_bundle($bundle) {
-}
-
-/**
- * Act on a bundle being renamed.
- *
- * @param $bundle_old
- * The old name of the bundle.
- * @param $bundle_new
- * The new name of the bundle.
- */
-function hook_field_storage_rename_bundle($bundle_old, $bundle_new) {
-}
-
-/**
* Act on creation of a new field.
*
* @param $field
@@ -1217,21 +1244,19 @@ function hook_field_storage_create_field($field) {
/**
* Act on deletion of a field.
*
- * @param $field_name
- * The name of the field being deleted.
+ * @param $field
+ * The field being deleted.
*/
-function hook_field_storage_delete_field($field_name) {
+function hook_field_storage_delete_field($field) {
}
/**
* Act on deletion of a field instance.
*
- * @param $field_name
- * The name of the field in the new instance.
- * @param $bundle
- * The name of the bundle in the new instance.
+ * @param $instance
+ * The instance being deleted.
*/
-function hook_field_storage_delete_instance($field_name, $bundle) {
+function hook_field_storage_delete_instance($instance) {
}
/**
diff --git a/modules/field/field.attach.inc b/modules/field/field.attach.inc
index 7571ee4..e2726a7 100644
--- a/modules/field/field.attach.inc
+++ b/modules/field/field.attach.inc
@@ -44,17 +44,16 @@ class FieldQueryException extends FieldException {}
* @{
* Implement a storage engine for Field API data.
*
- * The Field Attach API uses the Field Storage API to perform all
- * "database access". Each Field Storage API hook function defines a
- * primitive database operation such as read, write, or delete. The
- * default field storage module, field_sql_storage.module, uses the
- * local SQL database to implement these operations, but alternative
- * field storage engines can choose to represent the data in SQL
- * differently or use a completely different storage mechanism such as
- * a cloud-based database.
+ * The Field Attach API uses the Field Storage API to perform all "database
+ * access". Each Field Storage API hook function defines a primitive database
+ * operation such as read, write, or delete. The default field storage module,
+ * field_sql_storage.module, uses the local SQL database to implement these
+ * operations, but alternative field storage backends can choose to represent
+ * the data in SQL differently or use a completely different storage mechanism
+ * such as a cloud-based database.
*
- * The Drupal system variable field_storage_module identifies the
- * field storage module to use.
+ * Each field defines which storage backend it uses. The Drupal system variable
+ * 'field_default_storage' identifies the storage backend used by default.
*/
/**
@@ -525,9 +524,8 @@ function field_attach_form($obj_type, $object, &$form, &$form_state, $langcode =
* - 'deleted': If TRUE, the function will operate on deleted fields
* as well as non-deleted fields. If unset or FALSE, only
* non-deleted fields are operated on.
- * @returns
- * Loaded field values are added to $objects. Fields with no values should be
- * set as an empty array.
+ * @return
+ * Loaded field values are added to $objects.
*/
function field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT, $options = array()) {
$load_current = $age == FIELD_LOAD_CURRENT;
@@ -578,7 +576,7 @@ function field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT, $opti
if ($queried_objects) {
// The invoke order is:
// - hook_field_attach_pre_load()
- // - storage engine's hook_field_storage_load()
+ // - storage backend's hook_field_storage_load()
// - field-type module's hook_field_load()
// - hook_field_attach_load()
@@ -590,9 +588,39 @@ function field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT, $opti
$function($obj_type, $queried_objects, $age, $skip_fields, $options);
}
- // Invoke the storage engine's hook_field_storage_load(): the field storage
- // engine loads the rest.
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_load', $obj_type, $queried_objects, $age, $skip_fields, $options);
+ // Collect the storage backends used by the remaining fields in the objects.
+ $storages = array();
+ foreach ($queried_objects as $obj) {
+ list($id, $vid, $bundle) = field_extract_ids($obj_type, $obj);
+ if ($options['deleted']) {
+ $instances = field_read_instances(array('bundle' => $bundle), array('include_deleted' => $options['deleted']));
+ }
+ else {
+ $instances = field_info_instances($bundle);
+ }
+
+ foreach ($instances as $instance) {
+ if (!isset($options['field_id']) || $options['field_id'] == $instance['field_id']) {
+ $field_name = $instance['field_name'];
+ $field_id = $instance['field_id'];
+ // Make sure all fields are present at least as empty arrays.
+ if (!isset($queried_objects[$id]->{$field_name})) {
+ $queried_objects[$id]->{$field_name} = array();
+ }
+ // Collect the storage backend if the field has not been loaded yet.
+ if (!isset($skip_fields[$field_id])) {
+ $field = field_info_field_by_id($field_id);
+ $storages[$field['storage']['type']][$field_id][] = $load_current ? $id : $vid;
+ }
+ }
+ }
+ }
+
+ // Invoke hook_field_storage_load() on the relevant storage backends.
+ foreach ($storages as $storage => $fields) {
+ $storage_info = field_info_storage_types($storage);
+ module_invoke($storage_info['module'], 'field_storage_load', $obj_type, $queried_objects, $age, $fields, $options);
+ }
// Invoke field-type module's hook_field_load().
_field_invoke_multiple('load', $obj_type, $queried_objects, $age, $options);
@@ -791,6 +819,8 @@ function field_attach_insert($obj_type, $object) {
_field_invoke_default('insert', $obj_type, $object);
_field_invoke('insert', $obj_type, $object);
+ list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
+
// Let other modules act on inserting the object, accumulating saved
// fields along the way.
$skip_fields = array();
@@ -799,10 +829,26 @@ function field_attach_insert($obj_type, $object) {
$function($obj_type, $object, $skip_fields);
}
- // Field storage module saves any remaining unsaved fields.
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_write', $obj_type, $object, FIELD_STORAGE_INSERT, $skip_fields);
+ // Collect the storage backends used by the remaining fields in the objects.
+ $storages = array();
+ foreach (field_info_instances($bundle) as $instance) {
+ $field = field_info_field_by_id($instance['field_id']);
+ $field_id = $field['id'];
+ $field_name = $field['field_name'];
+ if (!empty($object->$field_name)) {
+ // Collect the storage backend if the field has not been written yet.
+ if (!isset($skip_fields[$field_id])) {
+ $storages[$field['storage']['type']][$field_id] = $field_id;
+ }
+ }
+ }
+
+ // Field storage backends save any remaining unsaved fields.
+ foreach ($storages as $storage => $fields) {
+ $storage_info = field_info_storage_types($storage);
+ module_invoke($storage_info['module'], 'field_storage_write', $obj_type, $object, FIELD_STORAGE_INSERT, $fields);
+ }
- list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
if ($cacheable) {
cache_clear_all("field:$obj_type:$id", 'cache_field');
}
@@ -819,6 +865,8 @@ function field_attach_insert($obj_type, $object) {
function field_attach_update($obj_type, $object) {
_field_invoke('update', $obj_type, $object);
+ list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
+
// Let other modules act on updating the object, accumulating saved
// fields along the way.
$skip_fields = array();
@@ -827,10 +875,30 @@ function field_attach_update($obj_type, $object) {
$function($obj_type, $object, $skip_fields);
}
- // Field storage module saves any remaining unsaved fields.
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_write', $obj_type, $object, FIELD_STORAGE_UPDATE, $skip_fields);
+ // Collect the storage backends used by the remaining fields in the objects.
+ $storages = array();
+ foreach (field_info_instances($bundle) as $instance) {
+ $field = field_info_field_by_id($instance['field_id']);
+ $field_id = $field['id'];
+ $field_name = $field['field_name'];
+ // Leave the field untouched if $object comes with no $field_name property,
+ // but empty the field if it comes as a NULL value or an empty array.
+ // Function property_exists() is slower, so we catch the more frequent
+ // cases where it's an empty array with the faster isset().
+ if (isset($object->$field_name) || property_exists($object, $field_name)) {
+ // Collect the storage backend if the field has not been written yet.
+ if (!isset($skip_fields[$field_id])) {
+ $storages[$field['storage']['type']][$field_id] = $field_id;
+ }
+ }
+ }
+
+ // Field storage backends save any remaining unsaved fields.
+ foreach ($storages as $storage => $fields) {
+ $storage_info = field_info_storage_types($storage);
+ module_invoke($storage_info['module'], 'field_storage_write', $obj_type, $object, FIELD_STORAGE_UPDATE, $fields);
+ }
- list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
if ($cacheable) {
cache_clear_all("field:$obj_type:$id", 'cache_field');
}
@@ -847,7 +915,22 @@ function field_attach_update($obj_type, $object) {
*/
function field_attach_delete($obj_type, $object) {
_field_invoke('delete', $obj_type, $object);
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete', $obj_type, $object);
+
+ list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
+
+ // Collect the storage backends used by the fields in the objects.
+ $storages = array();
+ foreach (field_info_instances($bundle) as $instance) {
+ $field = field_info_field_by_id($instance['field_id']);
+ $field_id = $field['id'];
+ $storages[$field['storage']['type']][$field_id] = $field_id;
+ }
+
+ // Field storage backends delete their data.
+ foreach ($storages as $storage => $fields) {
+ $storage_info = field_info_storage_types($storage);
+ module_invoke($storage_info['module'], 'field_storage_delete', $obj_type, $object, $fields);
+ }
// Let other modules act on deleting the object.
foreach (module_implements('field_attach_delete') as $module) {
@@ -855,7 +938,6 @@ function field_attach_delete($obj_type, $object) {
$function($obj_type, $object);
}
- list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
if ($cacheable) {
cache_clear_all("field:$obj_type:$id", 'cache_field');
}
@@ -872,7 +954,22 @@ function field_attach_delete($obj_type, $object) {
*/
function field_attach_delete_revision($obj_type, $object) {
_field_invoke('delete_revision', $obj_type, $object);
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete_revision', $obj_type, $object);
+
+ list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
+
+ // Collect the storage backends used by the fields in the objects.
+ $storages = array();
+ foreach (field_info_instances($bundle) as $instance) {
+ $field = field_info_field_by_id($instance['field_id']);
+ $field_id = $field['id'];
+ $storages[$field['storage']['type']][$field_id] = $field_id;
+ }
+
+ // Field storage backends delete their data.
+ foreach ($storages as $storage => $fields) {
+ $storage_info = field_info_storage_types($storage);
+ module_invoke($storage_info['module'], 'field_storage_delete_revision', $obj_type, $object, $fields);
+ }
// Let other modules act on deleting the revision.
foreach (module_implements('field_attach_delete_revision') as $module) {
@@ -977,7 +1074,8 @@ function field_attach_query($field_id, $conditions, $count, &$cursor = NULL, $ag
}
// If the request hasn't been handled, let the storage engine handle it.
if (!$skip_field) {
- $function = variable_get('field_storage_module', 'field_sql_storage') . '_field_storage_query';
+ $field = field_info_field_by_id($field_id);
+ $function = $field['storage']['module'] . '_field_storage_query';
$results = $function($field_id, $conditions, $count, $cursor, $age);
}
@@ -1194,8 +1292,6 @@ function field_attach_prepare_translation($node) {
* The name of the newly created bundle.
*/
function field_attach_create_bundle($bundle) {
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_create_bundle', $bundle);
-
// Clear the cache.
field_cache_clear();
@@ -1214,7 +1310,6 @@ function field_attach_create_bundle($bundle) {
* The new name of the bundle.
*/
function field_attach_rename_bundle($bundle_old, $bundle_new) {
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_rename_bundle', $bundle_old, $bundle_new);
db_update('field_config_instance')
->fields(array('bundle' => $bundle_new))
->condition('bundle', $bundle_old)
@@ -1243,12 +1338,15 @@ function field_attach_rename_bundle($bundle_old, $bundle_new) {
* The bundle to delete.
*/
function field_attach_delete_bundle($bundle) {
- // Delete the instances themseves
+ // First, delete the instances themseves.
$instances = field_info_instances($bundle);
foreach ($instances as $instance) {
field_delete_instance($instance['field_name'], $bundle);
}
+ // Clear the cache.
+ field_cache_clear();
+
// Let other modules act on deleting the bundle.
foreach (module_implements('field_attach_delete_bundle') as $module) {
$function = $module . '_field_attach_delete_bundle';
diff --git a/modules/field/field.crud.inc b/modules/field/field.crud.inc
index 210dad1..88e153a 100644
--- a/modules/field/field.crud.inc
+++ b/modules/field/field.crud.inc
@@ -76,6 +76,20 @@
* - settings (array)
* A sub-array of key/value pairs of field-type-specific settings. Each
* field type module defines and documents its own field settings.
+ * - storage (array)
+ * A sub-array of key/value pairs identifying the storage backend to use for
+ * the for the field.
+ * - type (string)
+ * The storage backend used by the field. Storage backends are defined
+ * by modules that implement hook_field_storage_info().
+ * - module (string, read-only)
+ * The name of the module that implements the storage backend.
+ * - active (integer, read-only)
+ * TRUE if the module that implements the storage backend is currently
+ * enabled, FALSE otherwise.
+ * - settings (array)
+ * A sub-array of key/value pairs of settings. Each storage backend
+ * defines and documents its own settings.
*
* Field Instance objects are (currently) represented as an array of
* key/value pairs. The object properties are:
@@ -196,6 +210,11 @@
* carefully, for it might seriously affect the site's performance.
* - settings: each omitted setting is given the default value defined in
* hook_field_info().
+ * - storage:
+ * - type: the storage backend specified in the 'field_default_storage'
+ * system variable.
+ * - settings: each omitted setting is given the default value specified in
+ * hook_field_storage_info().
* @return
* The $field structure with the id property filled in.
* @throw
@@ -223,12 +242,6 @@ function field_create_field($field) {
array('%name' => $field['field_name'])));
}
- // Check that the field type is known.
- $field_type = field_info_field_types($field['type']);
- if (!$field_type) {
- throw new FieldException(t('Attempt to create a field of unknown type %type.', array('%type' => $field['type'])));
- }
-
// Ensure the field name is unique over active and disabled fields.
// We do not care about deleted fields.
$prior_field = field_read_field($field['field_name'], array('include_inactive' => TRUE));
@@ -253,22 +266,40 @@ function field_create_field($field) {
'translatable' => FALSE,
'locked' => FALSE,
'settings' => array(),
+ 'storage' => array(),
+ 'deleted' => 0,
);
+ // Check that the field type is known.
+ $field_type = field_info_field_types($field['type']);
+ if (!$field_type) {
+ throw new FieldException(t('Attempt to create a field of unknown type %type.', array('%type' => $field['type'])));
+ }
// Create all per-field-type properties (needed here as long as we have
// settings that impact column definitions).
$field['settings'] += field_info_field_settings($field['type']);
$field['module'] = $field_type['module'];
$field['active'] = 1;
- $field['deleted'] = 0;
+ // Provide default storage.
+ $field['storage'] += array(
+ 'type' => variable_get('field_storage_default', 'field_sql_storage'),
+ 'settings' => array(),
+ );
+ // Check that the storage type is known.
+ $storage_type = field_info_storage_types($field['storage']['type']);
+ if (!$storage_type) {
+ throw new FieldException(t('Attempt to create a field with unknown storage type %type.', array('%type' => $field['storage']['type'])));
+ }
+ // Provide default storage settings.
+ $field['storage']['settings'] += field_info_storage_settings($field['storage']['type']);
+ $field['storage']['module'] = $storage_type['module'];
+ $field['storage']['active'] = 1;
// Collect storage information.
$schema = (array) module_invoke($field['module'], 'field_schema', $field);
$schema += array('columns' => array(), 'indexes' => array());
-
// 'columns' are hardcoded in the field type.
$field['columns'] = $schema['columns'];
-
// 'indexes' can be both hardcoded in the field type, and specified in the
// incoming $field definition.
$field += array(
@@ -280,19 +311,32 @@ function field_create_field($field) {
// have its own column and is not automatically populated when the field is
// read.
$data = $field;
- unset($data['columns'], $data['field_name'], $data['type'], $data['locked'], $data['module'], $data['cardinality'], $data['active'], $data['deleted']);
- $field['data'] = $data;
+ unset($data['columns'], $data['field_name'], $data['type'], $data['active'], $data['module'], $data['storage_type'], $data['storage_active'], $data['storage_module'], $data['locked'], $data['cardinality'], $data['deleted']);
- // Store the field and create the id.
- drupal_write_record('field_config', $field);
+ $record = array(
+ 'field_name' => $field['field_name'],
+ 'type' => $field['type'],
+ 'module' => $field['module'],
+ 'active' => $field['active'],
+ 'storage_type' => $field['storage']['type'],
+ 'storage_module' => $field['storage']['module'],
+ 'storage_active' => $field['storage']['active'],
+ 'locked' => $field['locked'],
+ 'data' => $data,
+ 'cardinality' => $field['cardinality'],
+ 'deleted' => $field['deleted'],
+ );
- // The 'data' property is not part of the public field record.
- unset($field['data']);
+ // Store the field and get the id back.
+ drupal_write_record('field_config', $record);
+ $field['id'] = $record['id'];
// Invoke hook_field_storage_create_field after the field is
// complete (e.g. it has its id).
try {
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_create_field', $field);
+ // Invoke hook_field_storage_create_field after
+ // drupal_write_record() sets the field id.
+ module_invoke($storage_type['module'], 'field_storage_create_field', $field);
}
catch (Exception $e) {
// If storage creation failed, remove the field_config record before
@@ -344,10 +388,13 @@ function field_update_field($field) {
$field += $prior_field;
$field['settings'] += $prior_field['settings'];
- // Field type cannot be changed.
+ // Some updates are always disallowed.
if ($field['type'] != $prior_field['type']) {
throw new FieldException("Cannot change an existing field's type.");
}
+ if ($field['storage']['type'] != $prior_field['storage']['type']) {
+ throw new FieldException("Cannot change an existing field's storage type.");
+ }
// Collect the new storage information, since what is in
// $prior_field may no longer be right.
@@ -442,7 +489,9 @@ function field_read_fields($params = array(), $include_additional = array()) {
$query->condition($key, $value);
}
if (!isset($include_additional['include_inactive']) || !$include_additional['include_inactive']) {
- $query->condition('fc.active', 1);
+ $query
+ ->condition('fc.active', 1)
+ ->condition('fc.storage_active', 1);
}
$include_deleted = (isset($include_additional['include_deleted']) && $include_additional['include_deleted']);
if (!$include_deleted) {
@@ -451,11 +500,20 @@ function field_read_fields($params = array(), $include_additional = array()) {
$fields = array();
$results = $query->execute();
- foreach ($results as $field) {
- // Extract serialized data.
- $data = unserialize($field['data']);
- unset($field['data']);
- $field += $data;
+ foreach ($results as $record) {
+ $field = unserialize($record['data']);
+ $field['id'] = $record['id'];
+ $field['field_name'] = $record['field_name'];
+ $field['type'] = $record['type'];
+ $field['module'] = $record['module'];
+ $field['active'] = $record['active'];
+ $field['storage']['type'] = $record['storage_type'];
+ $field['storage']['module'] = $record['storage_module'];
+ $field['storage']['active'] = $record['storage_active'];
+ $field['locked'] = $record['locked'];
+ $field['cardinality'] = $record['cardinality'];
+ $field['translatable'] = $record['translatable'];
+ $field['deleted'] = $record['deleted'];
module_invoke_all('field_read_field', $field);
@@ -489,8 +547,8 @@ function field_delete_field($field_name) {
}
}
- // Mark field storage for deletion.
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete_field', $field_name);
+ // Mark field data for deletion.
+ module_invoke($field['storage']['module'], 'field_storage_delete_field', $field);
// Mark the field for deletion.
db_update('field_config')
@@ -677,7 +735,7 @@ function _field_write_instance($instance, $update = FALSE) {
// not have its own column and is not automatically populated when the
// instance is read.
$data = $instance;
- unset($data['id'], $data['field_id'], $data['field_name'], $data['bundle'], $data['widget']['type'], $data['deleted']);
+ unset($data['id'], $data['field_id'], $data['field_name'], $data['bundle'], $data['widget']['type'], $data['widget']['module'], $data['widget']['active'], $data['deleted']);
$record = array(
'field_id' => $instance['field_id'],
@@ -751,8 +809,10 @@ function field_read_instances($params = array(), $include_additional = array())
$query->condition('fci.' . $key, $value);
}
if (!isset($include_additional['include_inactive']) || !$include_additional['include_inactive']) {
- $query->condition('fc.active', 1);
- $query->condition('fci.widget_active', 1);
+ $query
+ ->condition('fc.active', 1)
+ ->condition('fc.storage_active', 1)
+ ->condition('fci.widget_active', 1);
}
if (!isset($include_additional['include_deleted']) || !$include_additional['include_deleted']) {
$query->condition('fc.deleted', 0);
@@ -799,8 +859,10 @@ function field_delete_instance($field_name, $bundle) {
->condition('bundle', $bundle)
->execute();
- // Mark all data associated with the field for deletion.
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete_instance', $field_name, $bundle);
+ // Mark instance data for deletion.
+ $field = field_info_field($field_name);
+ module_invoke($field['storage']['module'], 'field_storage_delete_instance', $instance);
+
// Clear the cache.
field_cache_clear();
@@ -958,7 +1020,7 @@ function field_purge_data($obj_type, $object, $field, $instance) {
_field_invoke('delete', $obj_type, $object, $dummy, $dummy, $options);
// Tell the field storage system to purge the data.
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_purge', $obj_type, $object, $field, $instance);
+ module_invoke($field['storage']['module'], 'field_storage_purge', $obj_type, $object, $field, $instance);
// Let other modules act on purging the data.
foreach (module_implements('field_attach_purge') as $module) {
@@ -982,7 +1044,8 @@ function field_purge_instance($instance) {
->execute();
// Notify the storage engine.
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_purge_instance', $instance);
+ $field = field_info_field_by_id($instance['field_id']);
+ module_invoke($field['storage']['module'], 'field_storage_purge_instance', $instance);
// Clear the cache.
field_info_cache_clear();
@@ -1011,7 +1074,7 @@ function field_purge_field($field) {
->execute();
// Notify the storage engine.
- module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_purge_field', $field);
+ module_invoke($field['storage']['module'], 'field_storage_purge_field', $field);
// Clear the cache.
field_info_cache_clear();
diff --git a/modules/field/field.info.inc b/modules/field/field.info.inc
index aa5b4d0..8b273da 100644
--- a/modules/field/field.info.inc
+++ b/modules/field/field.info.inc
@@ -78,6 +78,7 @@ function _field_info_collate_types($reset = FALSE) {
'field types' => array(),
'widget types' => array(),
'formatter types' => array(),
+ 'storage types' => array(),
'fieldable types' => array(),
);
@@ -110,7 +111,7 @@ function _field_info_collate_types($reset = FALSE) {
}
drupal_alter('field_widget_info', $info['widget types']);
- // Populate formatters.
+ // Populate formatter types.
foreach (module_implements('field_formatter_info') as $module) {
$formatter_types = (array) module_invoke($module, 'field_formatter_info');
foreach ($formatter_types as $name => $formatter_info) {
@@ -124,6 +125,20 @@ function _field_info_collate_types($reset = FALSE) {
}
drupal_alter('field_formatter_info', $info['formatter types']);
+ // Populate storage types.
+ foreach (module_implements('field_storage_info') as $module) {
+ $storage_types = (array) module_invoke($module, 'field_storage_info');
+ foreach ($storage_types as $name => $storage_info) {
+ // Provide defaults.
+ $storage_info += array(
+ 'settings' => array(),
+ );
+ $info['storage types'][$name] = $storage_info;
+ $info['storage types'][$name]['module'] = $module;
+ }
+ }
+ drupal_alter('field_storage_info', $info['storage types']);
+
// Populate information about 'fieldable' entities.
foreach (module_implements('entity_info') as $module) {
$entities = (array) module_invoke($module, 'entity_info');
@@ -246,6 +261,7 @@ function _field_info_collate_fields($reset = FALSE) {
function _field_info_prepare_field($field) {
// Make sure all expected field settings are present.
$field['settings'] += field_info_field_settings($field['type']);
+ $field['storage']['settings'] += field_info_storage_settings($field['storage']['type']);
return $field;
}
@@ -371,8 +387,8 @@ function field_info_field_types($field_type = NULL) {
* returned.
* @return
* Either a widget type description, as provided by
- * hook_field_widget_info(), or an array of all existing widget
- * types, keyed by widget type name.
+ * hook_field_widget_info(), or an array of all existing widget types, keyed
+ * by widget type name.
*/
function field_info_widget_types($widget_type = NULL) {
$info = _field_info_collate_types();
@@ -394,8 +410,9 @@ function field_info_widget_types($widget_type = NULL) {
* (optional) A formatter type name. If ommitted, all formatter types will be
* returned.
* @return
- * Either a formatter type description, as provided by hook_field_formatter_info(),
- * or an array of all existing widget types, keyed by widget type name.
+ * Either a formatter type description, as provided by
+ * hook_field_formatter_info(), or an array of all existing formatter types,
+ * keyed by formatter type name.
*/
function field_info_formatter_types($formatter_type = NULL) {
$info = _field_info_collate_types();
@@ -411,6 +428,30 @@ function field_info_formatter_types($formatter_type = NULL) {
}
/**
+ * Return hook_field_storage_info() data.
+ *
+ * @param $storage_type
+ * (optional) A storage type name. If ommitted, all storage types will be
+ * returned.
+ * @return
+ * Either a storage type description, as provided by
+ * hook_field_storage_info(), or an array of all existing storage types,
+ * keyed by storage type name.
+ */
+function field_info_storage_types($storage_type = NULL) {
+ $info = _field_info_collate_types();
+ $storage_types = $info['storage types'];
+ if ($storage_type) {
+ if (isset($storage_types[$storage_type])) {
+ return $storage_types[$storage_type];
+ }
+ }
+ else {
+ return $storage_types;
+ }
+}
+
+/**
* Return hook_fieldable_info() data.
*
* @param $obj_type
@@ -574,8 +615,8 @@ function field_info_instance_settings($type) {
* @param $type
* A widget type name.
* @return
- * The field type's default settings, as provided by hook_field_info(), or an
- * empty array.
+ * The widget type's default settings, as provided by
+ * hook_field_widget_info(), or an empty array.
*/
function field_info_widget_settings($type) {
$info = field_info_widget_types($type);
@@ -588,8 +629,8 @@ function field_info_widget_settings($type) {
* @param $type
* A field formatter type name.
* @return
- * The field formatter's default settings, as provided by
- * hook_field_info(), or an empty array.
+ * The formatter type's default settings, as provided by
+ * hook_field_formatter_info(), or an empty array.
*/
function field_info_formatter_settings($type) {
$info = field_info_formatter_types($type);
@@ -597,5 +638,19 @@ function field_info_formatter_settings($type) {
}
/**
+ * Return a field formatter's default settings.
+ *
+ * @param $type
+ * A field storage type name.
+ * @return
+ * The storage type's default settings, as provided by
+ * hook_field_storage_info(), or an empty array.
+ */
+function field_info_storage_settings($type) {
+ $info = field_info_storage_types($type);
+ return isset($info['settings']) ? $info['settings'] : array();
+}
+
+/**
* @} End of "defgroup field_info"
*/
diff --git a/modules/field/field.install b/modules/field/field.install
index ba3db1d..c112ef1 100644
--- a/modules/field/field.install
+++ b/modules/field/field.install
@@ -28,41 +28,63 @@ function field_schema() {
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
- 'description' => 'The type of this field, coming from a field module',
+ 'description' => 'The type of this field.',
),
- 'locked' => array(
+ 'module' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The module that implements the field type.',
+ ),
+ 'active' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
- 'description' => '@TODO',
+ 'description' => 'Boolean indicating whether the module that implements the field type is enabled.',
),
- 'data' => array(
- 'type' => 'text',
- 'size' => 'medium',
+ 'storage_type' => array(
+ 'type' => 'varchar',
+ 'length' => 128,
'not null' => TRUE,
- 'serialize' => TRUE,
- 'description' => 'Field specific settings, for example maximum length',
+ 'description' => 'The storage backend for the field.',
),
- 'module' => array(
+ 'storage_module' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
+ 'description' => 'The module that implements the storage backend.',
),
- 'cardinality' => array(
+ 'storage_active' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
+ 'description' => 'Boolean indicating whether the module that implements the storage backend is enabled.',
),
- 'translatable' => array(
+ 'locked' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
+ 'description' => '@TODO',
),
- 'active' => array(
+ 'data' => array(
+ 'type' => 'text',
+ 'size' => 'medium',
+ 'not null' => TRUE,
+ 'serialize' => TRUE,
+ 'description' => 'Serialized data containing the field properties that do not warrant a dedicated column.',
+ ),
+ 'cardinality' => array(
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'translatable' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
@@ -77,14 +99,17 @@ function field_schema() {
),
'primary key' => array('id'),
'indexes' => array(
- // used by field_delete_field() among others
'field_name' => array('field_name'),
- // used by field_read_fields()
- 'active_deleted' => array('active', 'deleted'),
- // used by field_modules_disabled()
+ // Used by field_read_fields().
+ 'active' => array('active'),
+ 'storage_active' => array('storage_active'),
+ 'deleted' => array('deleted'),
+ // Used by field_modules_disabled().
'module' => array('module'),
- // used by field_associate_fields()
+ 'storage_module' => array('storage_module'),
+ // Used by field_associate_fields().
'type' => array('type'),
+ 'storage_type' => array('storage_type'),
),
);
$schema['field_config_instance'] = array(
@@ -124,13 +149,14 @@ function field_schema() {
),
'primary key' => array('id'),
'indexes' => array(
- // used by field_delete_instance()
+ // Used by field_delete_instance().
'field_name_bundle' => array('field_name', 'bundle'),
- // used by field_read_instances()
- 'widget_active_deleted' => array('widget_active', 'deleted'),
- // used by field_modules_disabled()
+ // Used by field_read_instances().
+ 'widget_active' => array('widget_active'),
+ 'deleted' => array('deleted'),
+ // Used by field_modules_disabled().
'widget_module' => array('widget_module'),
- // used by field_associate_fields()
+ // Used by field_associate_fields().
'widget_type' => array('widget_type'),
),
);
diff --git a/modules/field/field.module b/modules/field/field.module
index 75dc3d4..4d031e9 100644
--- a/modules/field/field.module
+++ b/modules/field/field.module
@@ -219,6 +219,10 @@ function field_modules_disabled($modules) {
->fields(array('active' => 0))
->condition('module', $module)
->execute();
+ db_update('field_config')
+ ->fields(array('storage_active' => 0))
+ ->condition('storage_module', $module)
+ ->execute();
db_update('field_config_instance')
->fields(array('widget_active' => 0))
->condition('widget_module', $module)
@@ -234,25 +238,32 @@ function field_modules_disabled($modules) {
* The name of the module to update on.
*/
function field_associate_fields($module) {
- $module_fields = module_invoke($module, 'field_info');
- if ($module_fields) {
- foreach ($module_fields as $name => $field_info) {
- watchdog('field', 'Updating field type %type with module %module.', array('%type' => $name, '%module' => $module));
- db_update('field_config')
- ->fields(array('module' => $module, 'active' => 1))
- ->condition('type', $name)
- ->execute();
- }
+ // Associate field types.
+ $field_types =(array) module_invoke($module, 'field_info');
+ foreach ($field_types as $name => $field_info) {
+ watchdog('field', 'Updating field type %type with module %module.', array('%type' => $name, '%module' => $module));
+ db_update('field_config')
+ ->fields(array('module' => $module, 'active' => 1))
+ ->condition('type', $name)
+ ->execute();
}
- $module_widgets = module_invoke($module, 'widget_info');
- if ($module_widgets) {
- foreach ($module_widgets as $name => $widget_info) {
- watchdog('field', 'Updating widget type %type with module %module.', array('%type' => $name, '%module' => $module));
- db_update('field_config_instance')
- ->fields(array('widget_module' => $module, 'widget_active' => 1))
- ->condition('widget_type', $name)
- ->execute();
- }
+ // Associate storage backends.
+ $storage_types = (array) module_invoke($module, 'field_storage_info');
+ foreach ($storage_types as $name => $storage_info) {
+ watchdog('field', 'Updating field storage %type with module %module.', array('%type' => $name, '%module' => $module));
+ db_update('field_config')
+ ->fields(array('storage_module' => $module, 'storage_active' => 1))
+ ->condition('storage_type', $name)
+ ->execute();
+ }
+ // Associate widget types.
+ $widget_types = (array) module_invoke($module, 'field_widget_info');
+ foreach ($widget_types as $name => $widget_info) {
+ watchdog('field', 'Updating widget type %type with module %module.', array('%type' => $name, '%module' => $module));
+ db_update('field_config_instance')
+ ->fields(array('widget_module' => $module, 'widget_active' => 1))
+ ->condition('widget_type', $name)
+ ->execute();
}
}
diff --git a/modules/field/field.test b/modules/field/field.test
index 2523c8b..c77d790 100644
--- a/modules/field/field.test
+++ b/modules/field/field.test
@@ -10,6 +10,18 @@
* Parent class for Field API tests.
*/
class FieldTestCase extends DrupalWebTestCase {
+ var $default_storage = 'field_sql_storage';
+
+ /**
+ * Set the default field storage backend for fields created during tests.
+ */
+ function setUp() {
+ // Call parent::setUp().
+ $args = func_get_args();
+ call_user_func_array(array('parent', 'setUp'), $args);
+ // Set default storage backend.
+ variable_set('field_storage_default', $this->default_storage);
+ }
/**
* Generate random values for a field_test field.
@@ -212,6 +224,56 @@ class FieldAttachStorageTestCase extends FieldAttachTestCase {
}
/**
+ * Test saving and loading fields using different storage backends.
+ */
+ function testFieldAttachSaveLoadDifferentStorage() {
+ $entity_type = 'test_entity';
+ $langcode = FIELD_LANGUAGE_NONE;
+
+ // Create two fields using different storage backends, and their instances.
+ $fields = array(
+ array(
+ 'field_name' => 'field_1',
+ 'type' => 'test_field',
+ 'cardinality' => 4,
+ 'storage' => array('type' => 'field_sql_storage')
+ ),
+ array(
+ 'field_name' => 'field_2',
+ 'type' => 'test_field',
+ 'cardinality' => 4,
+ 'storage' => array('type' => 'field_test_storage')
+ ),
+ );
+ foreach ($fields as $field) {
+ field_create_field($field);
+ $instance = array(
+ 'field_name' => $field['field_name'],
+ 'bundle' => 'test_bundle',
+ );
+ field_create_instance($instance);
+ }
+
+ $entity_init = field_test_create_stub_entity();
+
+ // Create entity and insert random values.
+ $entity = clone($entity_init);
+ $values = array();
+ foreach ($fields as $field) {
+ $values[$field['field_name']] = $this->_generateTestFieldValues($this->field['cardinality']);
+ $entity->{$field['field_name']}[$langcode] = $values[$field['field_name']];
+ }
+ field_attach_insert($entity_type, $entity);
+
+ // Check that values are loaded as expected.
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ foreach ($fields as $field) {
+ $this->assertEqual($values[$field['field_name']], $entity->{$field['field_name']}[$langcode], t('%storage storage: expected values were found.', array('%storage' => $field['storage']['type'])));
+ }
+ }
+
+ /**
* Tests insert and update with missing or NULL fields.
*/
function testFieldAttachSaveMissingData() {
@@ -225,17 +287,17 @@ class FieldAttachStorageTestCase extends FieldAttachTestCase {
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
- $this->assertTrue(empty($entity->{$this->field_name}[$langcode]), t('Insert: missing field results in no value saved'));
+ $this->assertTrue(empty($entity->{$this->field_name}), t('Insert: missing field results in no value saved'));
// Insert: Field is NULL.
field_cache_clear();
$entity = clone($entity_init);
- $entity->{$this->field_name}[$langcode] = NULL;
+ $entity->{$this->field_name} = NULL;
field_attach_insert($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
- $this->assertTrue(empty($entity->{$this->field_name}[$langcode]), t('Insert: NULL field results in no value saved'));
+ $this->assertTrue(empty($entity->{$this->field_name}), t('Insert: NULL field results in no value saved'));
// Add some real data.
field_cache_clear();
@@ -260,12 +322,33 @@ class FieldAttachStorageTestCase extends FieldAttachTestCase {
// Update: Field is NULL. Data should be wiped.
field_cache_clear();
$entity = clone($entity_init);
- $entity->{$this->field_name}[$langcode] = NULL;
+ $entity->{$this->field_name} = NULL;
field_attach_update($entity_type, $entity);
$entity = clone($entity_init);
field_attach_load($entity_type, array($entity->ftid => $entity));
- $this->assertTrue(empty($entity->{$this->field_name}[$langcode]), t('Update: NULL field removes existing values'));
+ $this->assertTrue(empty($entity->{$this->field_name}), t('Update: NULL field removes existing values'));
+
+ // Re-add some data.
+ field_cache_clear();
+ $entity = clone($entity_init);
+ $values = $this->_generateTestFieldValues(1);
+ $entity->{$this->field_name}[$langcode] = $values;
+ field_attach_update($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertEqual($entity->{$this->field_name}[$langcode], $values, t('Field data saved'));
+
+ // Update: Field is empty array. Data should be wiped.
+ field_cache_clear();
+ $entity = clone($entity_init);
+ $entity->{$this->field_name} = array();
+ field_attach_update($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertTrue(empty($entity->{$this->field_name}), t('Update: empty array removes existing values'));
}
/**
@@ -391,7 +474,7 @@ class FieldAttachStorageTestCase extends FieldAttachTestCase {
$this->assertIdentical($this->instance['bundle'], $new_bundle, "Bundle name has been updated in the instance.");
// Verify the field data is present on load.
- $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ $entity = field_test_create_stub_entity(0, 0, $new_bundle);
field_attach_load($entity_type, array(0 => $entity));
$this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], "Bundle name has been updated in the field storage");
}
@@ -956,6 +1039,7 @@ class FieldInfoTestCase extends FieldTestCase {
$field_test_info = field_test_field_info();
$formatter_info = field_test_field_formatter_info();
$widget_info = field_test_field_widget_info();
+ $storage_info = field_test_field_storage_info();
$info = field_info_field_types();
foreach ($field_test_info as $t_key => $field_type) {
@@ -981,6 +1065,14 @@ class FieldInfoTestCase extends FieldTestCase {
$this->assertEqual($info[$w_key]['module'], 'field_test', t("Widget type field_test module appears"));
}
+ $info = field_info_storage_types();
+ foreach ($storage_info as $s_key => $storage) {
+ foreach ($storage as $key => $val) {
+ $this->assertEqual($info[$s_key][$key], $val, t("Storage type $s_key key $key is $val"));
+ }
+ $this->assertEqual($info[$s_key]['module'], 'field_test', t("Storage type field_test module appears"));
+ }
+
// Verify that no unexpected instances exist.
$core_fields = field_info_fields();
$instances = field_info_instances(FIELD_TEST_BUNDLE);
@@ -1473,6 +1565,9 @@ class FieldCrudTestCase extends FieldTestCase {
$field_type = field_info_field_types($field_definition['type']);
$this->assertIdentical($record['data']['settings'], $field_type['settings'], t('Default field settings have been written.'));
+ // Ensure that default storage was set.
+ $this->assertEqual($record['storage_type'], variable_get('field_storage_default'), t('The field type is properly saved.'));
+
// Guarantee that the name is unique.
try {
field_create_field($field_definition);
@@ -1565,16 +1660,13 @@ class FieldCrudTestCase extends FieldTestCase {
*/
function testCreateFieldFail() {
$field_name = 'duplicate';
- $field_definition = array('field_name' => $field_name, 'type' => 'test_field');
+ $field_definition = array('field_name' => $field_name, 'type' => 'test_field', 'storage' => array('type' => 'field_test_storage_failure'));
$query = db_select('field_config')->condition('field_name', $field_name)->countQuery();
// The field does not appear in field_config.
$count = $query->execute()->fetchField();
$this->assertEqual($count, 0, 'A field_config row for the field does not exist.');
- // Make field creation fail.
- variable_set('field_storage_module', 'field_test');
-
// Try to create the field.
try {
$field = field_create_field($field_definition);
@@ -1679,8 +1771,7 @@ class FieldCrudTestCase extends FieldTestCase {
// Make sure that the field is marked as deleted when it is specifically
// loaded.
- $fields = field_read_fields(array(), array('include_deleted' => TRUE));
- $field = current($field);
+ $field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE));
$this->assertTrue(!empty($field['deleted']), t('A deleted field is marked for deletion.'));
// Make sure that this field's instance is marked as deleted when it is
@@ -1829,6 +1920,69 @@ class FieldCrudTestCase extends FieldTestCase {
$this->pass(t("An unchangeable setting cannot be updated."));
}
}
+
+ /**
+ * Test that fields are properly marked active or inactive.
+ */
+ function testActive() {
+ $field_definition = array(
+ 'field_name' => 'field_1',
+ 'type' => 'test_field',
+ // For this test, we need a storage backend provided by a different
+ // module than field_test.module.
+ 'storage' => array(
+ 'type' => 'field_sql_storage',
+ ),
+ );
+ field_create_field($field_definition);
+
+ // Test disabling and enabling:
+ // - the field type module,
+ // - the storage module,
+ // - both.
+ $this->_testActiveHelper($field_definition, array('field_test'));
+ $this->_testActiveHelper($field_definition, array('field_sql_storage'));
+ $this->_testActiveHelper($field_definition, array('field_test', 'field_sql_storage'));
+ }
+
+ /**
+ * Helper function for testActive().
+ *
+ * Test dependency between a field and a set of modules.
+ *
+ * @param $field_definition
+ * A field definition.
+ * @param $modules
+ * An aray of module names. The field will be tested to be inactive as long
+ * as any of those modules is disabled.
+ */
+ function _testActiveHelper($field_definition, $modules) {
+ $field_name = $field_definition['field_name'];
+
+ // Read the field.
+ $field = field_read_field($field_name);
+ $this->assertTrue($field_definition <= $field, t('The field was properly read.'));
+
+ module_disable($modules);
+
+ $fields = field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE));
+ $this->assertTrue(isset($fields[$field_name]) && $field_definition < $field, t('The field is properly read when explicitly fetching inactive fields.'));
+
+ // Re-enable modules one by one, and check that the field is still inactive
+ // while some modules remain disabled.
+ while ($modules) {
+ $field = field_read_field($field_name);
+ $this->assertTrue(empty($field), t('%modules disabled. The field is marked inactive.', array('%modules' => implode(', ', $modules))));
+
+ $module = array_shift($modules);
+ module_enable(array($module));
+ }
+
+ // Check that the field is active again after all modules have been
+ // enabled.
+ $field = field_read_field($field_name);
+ $this->assertTrue($field_definition <= $field, t('The field was was marked active.'));
+ }
}
class FieldInstanceCrudTestCase extends FieldTestCase {
@@ -1844,6 +1998,7 @@ class FieldInstanceCrudTestCase extends FieldTestCase {
function setUp() {
parent::setUp('field_test');
+
$this->field = array(
'field_name' => drupal_strtolower($this->randomName()),
'type' => 'test_field',
@@ -2430,7 +2585,7 @@ class FieldBulkDeleteTestCase extends FieldTestCase {
// The field still exists, not deleted, because it has a second instance.
$fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1, 'include_inactive' => 1));
- $this->assertEqual($field, $fields[$field['id']], 'The field exists and is not deleted');
+ $this->assertTrue(isset($fields[$field['id']]), 'The field exists and is not deleted');
}
/**
@@ -2452,7 +2607,7 @@ class FieldBulkDeleteTestCase extends FieldTestCase {
// The field still exists, not deleted, because it was never deleted.
$fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1, 'include_inactive' => 1));
- $this->assertEqual($field, $fields[$field['id']], 'The field exists and is not deleted');
+ $this->assertTrue(isset($fields[$field['id']]), 'The field exists and is not deleted');
}
// Delete the field.
diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.install b/modules/field/modules/field_sql_storage/field_sql_storage.install
index 67b7e4a..7ab6a8c 100644
--- a/modules/field/modules/field_sql_storage/field_sql_storage.install
+++ b/modules/field/modules/field_sql_storage/field_sql_storage.install
@@ -37,7 +37,9 @@ function field_sql_storage_schema() {
$fields = field_read_fields(array(), array('include_deleted' => TRUE, 'include_inactive' => TRUE));
drupal_load('module', 'field_sql_storage');
foreach ($fields as $field) {
- $schema += _field_sql_storage_schema($field);
+ if ($field['storage']['type'] == 'field_sql_storage') {
+ $schema += _field_sql_storage_schema($field);
+ }
}
}
return $schema;
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 97d372f..09dee36 100644
--- a/modules/field/modules/field_sql_storage/field_sql_storage.module
+++ b/modules/field/modules/field_sql_storage/field_sql_storage.module
@@ -18,6 +18,18 @@ function field_sql_storage_help($path, $arg) {
}
/**
+ * Implement hook_field_storage_info().
+ */
+function field_sql_storage_field_storage_info() {
+ return array(
+ 'field_sql_storage' => array(
+ 'label' => t('Default SQL storage'),
+ 'description' => t('Stores fields in the local SQL database, using per-field tables.'),
+ ),
+ );
+}
+
+/**
* Generate a table name for a field data table.
*
* @param $field
@@ -264,9 +276,7 @@ function field_sql_storage_field_storage_update_field($field, $prior_field, $has
/**
* Implement hook_field_storage_delete_field().
*/
-function field_sql_storage_field_storage_delete_field($field_name) {
- $field = field_info_field($field_name);
-
+function field_sql_storage_field_storage_delete_field($field) {
// Mark all data associated with the field for deletion.
$field['deleted'] = 0;
$table = _field_sql_storage_tablename($field);
@@ -288,34 +298,11 @@ function field_sql_storage_field_storage_delete_field($field_name) {
/**
* Implement hook_field_storage_load().
*/
-function field_sql_storage_field_storage_load($obj_type, $objects, $age, $skip_fields, $options) {
+function field_sql_storage_field_storage_load($obj_type, $objects, $age, $fields, $options) {
$etid = _field_sql_storage_etid($obj_type);
$load_current = $age == FIELD_LOAD_CURRENT;
- // Gather ids needed for each field.
- $field_ids = array();
- $delta_count = array();
- foreach ($objects as $obj) {
- list($id, $vid, $bundle) = field_extract_ids($obj_type, $obj);
-
- if ($options['deleted']) {
- $instances = field_read_instances(array('bundle' => $bundle), array('include_deleted' => $options['deleted']));
- }
- else {
- $instances = field_info_instances($bundle);
- }
-
- foreach ($instances as $instance) {
- $field_name = $instance['field_name'];
- if (!isset($skip_fields[$instance['field_id']]) && (!isset($options['field_id']) || $options['field_id'] == $instance['field_id'])) {
- $objects[$id]->{$field_name} = array();
- $field_ids[$instance['field_id']][] = $load_current ? $id : $vid;
- $delta_count[$id][$field_name] = array();
- }
- }
- }
-
- foreach ($field_ids as $field_id => $ids) {
+ foreach ($fields as $field_id => $ids) {
$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);
@@ -333,12 +320,13 @@ function field_sql_storage_field_storage_load($obj_type, $objects, $age, $skip_f
$results = $query->execute();
+ $delta_count = array();
foreach ($results as $row) {
- if (!isset($delta_count[$row->entity_id][$field_name][$row->language])) {
- $delta_count[$row->entity_id][$field_name][$row->language] = 0;
+ if (!isset($delta_count[$row->entity_id][$row->language])) {
+ $delta_count[$row->entity_id][$row->language] = 0;
}
- if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name][$row->language] < $field['cardinality']) {
+ if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->language] < $field['cardinality']) {
$item = array();
// For each column declared by the field, populate the item
// from the prefixed database column.
@@ -349,7 +337,7 @@ function field_sql_storage_field_storage_load($obj_type, $objects, $age, $skip_f
// Add the item to the field values for the entity.
$objects[$row->entity_id]->{$field_name}[$row->language][] = $item;
- $delta_count[$row->entity_id][$field_name][$row->language]++;
+ $delta_count[$row->entity_id][$row->language]++;
}
}
}
@@ -358,41 +346,30 @@ function field_sql_storage_field_storage_load($obj_type, $objects, $age, $skip_f
/**
* Implement hook_field_storage_write().
*/
-function field_sql_storage_field_storage_write($obj_type, $object, $op, $skip_fields) {
+function field_sql_storage_field_storage_write($obj_type, $object, $op, $fields) {
list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
$etid = _field_sql_storage_etid($obj_type);
- $instances = field_info_instances($bundle);
- foreach ($instances as $instance) {
- $field_name = $instance['field_name'];
- if (isset($skip_fields[$instance['field_id']])) {
- continue;
- }
-
- $field = field_info_field($field_name);
+ foreach ($fields as $field_id) {
+ $field = field_info_field_by_id($field_id);
+ $field_name = $field['field_name'];
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
- // Leave the field untouched if $object comes with no $field_name property.
- // Empty the field if $object->$field_name is NULL or an empty array.
-
- // Function property_exists() is slower, so we catch the more frequent cases
- // where it's an empty array with the faster isset().
- if (isset($object->$field_name) || property_exists($object, $field_name)) {
- $available_languages = field_multilingual_available_languages($obj_type, $field);
- $available_translations = is_array($object->$field_name) ? array_intersect($available_languages, array_keys($object->$field_name)) : FALSE;
-
- // Delete and insert, rather than update, in case a value was added.
- // If no translation is available, empty the field for all the available languages.
- if ($op == FIELD_STORAGE_UPDATE && count($available_translations)) {
- $languages = empty($object->$field_name) ? $available_languages : $available_translations;
+ $all_languages = field_multilingual_available_languages($obj_type, $field);
+ $field_languages = array_intersect($all_languages, array_keys((array) $object->$field_name));
+ // Delete and insert, rather than update, in case a value was added.
+ if ($op == FIELD_STORAGE_UPDATE) {
+ // Delete languages present in the incoming $object->$field_name.
+ // Delete all languages if $object->$field_name is empty.
+ $languages = !empty($object->$field_name) ? $field_languages : $all_languages;
+ if ($languages) {
db_delete($table_name)
->condition('etid', $etid)
->condition('entity_id', $id)
->condition('language', $languages, 'IN')
->execute();
-
if (isset($vid)) {
db_delete($revision_name)
->condition('etid', $etid)
@@ -402,46 +379,48 @@ function field_sql_storage_field_storage_write($obj_type, $object, $op, $skip_fi
->execute();
}
}
+ }
- if (!empty($available_translations)) {
- // Prepare the multi-insert query.
- $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta', 'language');
+ // Prepare the multi-insert query.
+ $do_insert = FALSE;
+ $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta', 'language');
+ foreach ($field['columns'] as $column => $attributes) {
+ $columns[] = _field_sql_storage_columnname($field_name, $column);
+ }
+ $query = db_insert($table_name)->fields($columns);
+ if (isset($vid)) {
+ $revision_query = db_insert($revision_name)->fields($columns);
+ }
+
+ foreach ($field_languages as $langcode) {
+ $items = (array) $object->{$field_name}[$langcode];
+ $delta_count = 0;
+ foreach ($items as $delta => $item) {
+ // We now know we have someting to insert.
+ $do_insert = TRUE;
+ $record = array(
+ 'etid' => $etid,
+ 'entity_id' => $id,
+ 'revision_id' => $vid,
+ 'bundle' => $bundle,
+ 'delta' => $delta,
+ 'language' => $langcode,
+ );
foreach ($field['columns'] as $column => $attributes) {
- $columns[] = _field_sql_storage_columnname($field_name, $column);
+ $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL;
}
- $query = db_insert($table_name)->fields($columns);
+ $query->values($record);
if (isset($vid)) {
- $revision_query = db_insert($revision_name)->fields($columns);
+ $revision_query->values($record);
}
- foreach ($available_translations as $langcode) {
- if ($items = $object->{$field_name}[$langcode]) {
- $delta_count = 0;
- foreach ($items as $delta => $item) {
- $record = array(
- 'etid' => $etid,
- 'entity_id' => $id,
- 'revision_id' => $vid,
- 'bundle' => $bundle,
- 'delta' => $delta,
- 'language' => $langcode,
- );
- foreach ($field['columns'] as $column => $attributes) {
- $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL;
- }
- $query->values($record);
- if (isset($vid)) {
- $revision_query->values($record);
- }
-
- if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) {
- break;
- }
- }
- }
+ if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) {
+ break;
}
+ }
- // Execute the insert.
+ // Execute the query if we have values to insert.
+ if ($do_insert) {
$query->execute();
if (isset($vid)) {
$revision_query->execute();
@@ -456,14 +435,15 @@ function field_sql_storage_field_storage_write($obj_type, $object, $op, $skip_fi
*
* This function deletes data for all fields for an object from the database.
*/
-function field_sql_storage_field_storage_delete($obj_type, $object) {
+function field_sql_storage_field_storage_delete($obj_type, $object, $fields) {
list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
$etid = _field_sql_storage_etid($obj_type);
- $instances = field_info_instances($bundle);
- foreach ($instances as $instance) {
- $field = field_info_field($instance['field_name']);
- field_sql_storage_field_storage_purge($obj_type, $object, $field, $instance);
+ foreach (field_info_instances($bundle) as $instance) {
+ if (isset($fields[$instance['field_id']])) {
+ $field = field_info_field_by_id($instance['field_id']);
+ field_sql_storage_field_storage_purge($obj_type, $object, $field, $instance);
+ }
}
}
@@ -477,7 +457,6 @@ function field_sql_storage_field_storage_purge($obj_type, $object, $field, $inst
list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
$etid = _field_sql_storage_etid($obj_type);
- $field = field_info_field_by_id($field['id']);
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
db_delete($table_name)
@@ -553,16 +532,16 @@ function field_sql_storage_field_storage_query($field_id, $conditions, $count, &
$value = _field_sql_storage_etid($value);
}
}
-
- $query->condition($column, $value, $operator);
-
+ // Track condition on 'deleted'.
if ($column == 'deleted') {
- $deleted = $value;
+ $condition_deleted = TRUE;
}
+
+ $query->condition($column, $value, $operator);
}
// Exclude deleted data unless we have a condition on it.
- if (!isset($deleted)) {
+ if (!isset($condition_deleted)) {
$query->condition('deleted', 0);
}
@@ -610,15 +589,13 @@ function field_sql_storage_field_storage_query($field_id, $conditions, $count, &
*
* This function actually deletes the data from the database.
*/
-function field_sql_storage_field_storage_delete_revision($obj_type, $object) {
+function field_sql_storage_field_storage_delete_revision($obj_type, $object, $fields) {
list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
$etid = _field_sql_storage_etid($obj_type);
if (isset($vid)) {
- $instances = field_info_instances($bundle);
- foreach ($instances as $instance) {
- $field_name = $instance['field_name'];
- $field = field_read_field($field_name);
+ foreach ($fields as $field_id) {
+ $field = field_info_field_by_id($field_id);
$revision_name = _field_sql_storage_revision_tablename($field);
db_delete($revision_name)
->condition('etid', $etid)
@@ -634,37 +611,40 @@ function field_sql_storage_field_storage_delete_revision($obj_type, $object) {
*
* This function simply marks for deletion all data associated with the field.
*/
-function field_sql_storage_field_storage_delete_instance($field_name, $bundle) {
- $field = field_read_field($field_name);
+function field_sql_storage_field_storage_delete_instance($instance) {
+ $field = field_info_field($instance['field_name']);
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
db_update($table_name)
->fields(array('deleted' => 1))
- ->condition('bundle', $bundle)
+ ->condition('bundle', $instance['bundle'])
->execute();
db_update($revision_name)
->fields(array('deleted' => 1))
- ->condition('bundle', $bundle)
+ ->condition('bundle', $instance['bundle'])
->execute();
}
/**
- * Implement hook_field_storage_rename_bundle().
+ * Implement hook_field_attach_rename_bundle().
*/
-function field_sql_storage_field_storage_rename_bundle($bundle_old, $bundle_new) {
- $instances = field_info_instances($bundle_old);
+function field_sql_storage_field_attach_rename_bundle($bundle_old, $bundle_new) {
+ // We need to account for deleted or inactive fields and instances.
+ $instances = field_read_instances(array('bundle' => $bundle_new), array('include_deleted' => TRUE, 'include_inactive' => TRUE));
foreach ($instances as $instance) {
- $field = field_read_field($instance['field_name']);
- $table_name = _field_sql_storage_tablename($field);
- $revision_name = _field_sql_storage_revision_tablename($field);
- db_update($table_name)
- ->fields(array('bundle' => $bundle_new))
- ->condition('bundle', $bundle_old)
- ->execute();
- db_update($revision_name)
- ->fields(array('bundle' => $bundle_new))
- ->condition('bundle', $bundle_old)
- ->execute();
+ $field = field_info_field_by_id($instance['field_id']);
+ if ($field['storage']['type'] == 'field_sql_storage') {
+ $table_name = _field_sql_storage_tablename($field);
+ $revision_name = _field_sql_storage_revision_tablename($field);
+ db_update($table_name)
+ ->fields(array('bundle' => $bundle_new))
+ ->condition('bundle', $bundle_old)
+ ->execute();
+ db_update($revision_name)
+ ->fields(array('bundle' => $bundle_new))
+ ->condition('bundle', $bundle_old)
+ ->execute();
+ }
}
}
diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.test b/modules/field/modules/field_sql_storage/field_sql_storage.test
index 5f8a30a..7108913 100644
--- a/modules/field/modules/field_sql_storage/field_sql_storage.test
+++ b/modules/field/modules/field_sql_storage/field_sql_storage.test
@@ -183,8 +183,8 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase {
$this->assertTrue(empty($rev_values), "All values for all revisions are stored in revision table {$this->revision_table}");
// Check that update leaves the field data untouched if
- // $object->{$field_name} has no language key.
- unset($entity->{$this->field_name}[$langcode]);
+ // $object->{$field_name} is absent.
+ unset($entity->{$this->field_name});
field_attach_update($entity_type, $entity);
$rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC);
foreach ($values as $delta => $value) {
@@ -194,7 +194,7 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase {
}
// Check that update with an empty $object->$field_name empties the field.
- $entity->{$this->field_name}[$langcode] = NULL;
+ $entity->{$this->field_name} = NULL;
field_attach_update($entity_type, $entity);
$rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC);
$this->assertEqual(count($rows), 0, t("Update with an empty field_name entry empties the field."));
@@ -217,7 +217,7 @@ class FieldSqlStorageTestCase extends DrupalWebTestCase {
$this->assertEqual($count, 0, 'Missing field results in no inserts');
// Insert: Field is NULL
- $entity->{$this->field_name}[$langcode] = NULL;
+ $entity->{$this->field_name} = NULL;
field_attach_insert($entity_type, $entity);
$count = db_select($this->table)
->countQuery()
diff --git a/modules/simpletest/tests/field_test.module b/modules/simpletest/tests/field_test.module
index 1f21e5d..93b8084 100644
--- a/modules/simpletest/tests/field_test.module
+++ b/modules/simpletest/tests/field_test.module
@@ -641,6 +641,436 @@ function field_test_entity_info_translatable($obj_type = NULL, $translatable = N
}
/**
+ *
+ * 'Field storage' API.
+ *
+ */
+
+/**
+ * Implement hook_field_storage_info().
+ */
+function field_test_field_storage_info() {
+ return array(
+ 'field_test_storage' => array(
+ 'label' => t('Test storage'),
+ 'description' => t('Dummy test storage backend. Stores field values in the variable table.'),
+ ),
+ 'field_test_storage_failure' => array(
+ 'label' => t('Test storage failure'),
+ 'description' => t('Dummy test storage backend. Always fails to create fields.'),
+ ),
+ );
+}
+
+/**
+ * Helper function: store or retrieve data from the 'storage backend'.
+ */
+function _field_test_storage_data($data = NULL) {
+ if (is_null($data)) {
+ return variable_get('field_test_storage_data', array());
+ }
+ else {
+ variable_set('field_test_storage_data', $data);
+ }
+}
+
+/**
+ * Implement hook_field_storage_load().
+ */
+function field_test_field_storage_load($obj_type, $objects, $age, $fields, $options) {
+ $data = _field_test_storage_data();
+
+ $load_current = $age == FIELD_LOAD_CURRENT;
+
+ foreach ($fields as $field_id => $ids) {
+ $field = field_info_field_by_id($field_id);
+ $field_name = $field['field_name'];
+ $field_data = $data[$field['id']];
+ $sub_table = $load_current ? 'current' : 'revisions';
+ $delta_count = array();
+ foreach ($field_data[$sub_table] as $row) {
+ if ($row->type == $obj_type && (!$row->deleted || $options['deleted'])) {
+ if (($load_current && in_array($row->entity_id, $ids)) || (!$load_current && in_array($row->revision_id, $ids))) {
+ if (in_array($row->language, field_multilingual_available_languages($obj_type, $field))) {
+ if (!isset($delta_count[$row->entity_id][$row->language])) {
+ $delta_count[$row->entity_id][$row->language] = 0;
+ }
+ if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->language] < $field['cardinality']) {
+ $item = array();
+ foreach ($field['columns'] as $column => $attributes) {
+ $item[$column] = $row->{$column};
+ }
+ $objects[$row->entity_id]->{$field_name}[$row->language][] = $item;
+ $delta_count[$row->entity_id][$row->language]++;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implement hook_field_storage_write().
+ */
+function field_test_field_storage_write($obj_type, $object, $op, $fields) {
+ $data = _field_test_storage_data();
+
+ list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
+
+ foreach ($fields as $field_id) {
+ $field = field_info_field_by_id($field_id);
+ $field_name = $field['field_name'];
+ $field_data = &$data[$field_id];
+
+ $all_languages = field_multilingual_available_languages($obj_type, $field);
+ $field_languages = array_intersect($all_languages, array_keys((array) $object->$field_name));
+
+ // Delete and insert, rather than update, in case a value was added.
+ if ($op == FIELD_STORAGE_UPDATE) {
+ // Delete languages present in the incoming $object->$field_name.
+ // Delete all languages if $object->$field_name is empty.
+ $languages = !empty($object->$field_name) ? $field_languages : $all_languages;
+ if ($languages) {
+ foreach ($field_data['current'] as $key => $row) {
+ if ($row->type == $obj_type && $row->entity_id == $id && in_array($row->language, $languages)) {
+ unset($field_data['current'][$key]);
+ }
+ }
+ if (isset($vid)) {
+ foreach ($field_data['revisions'] as $key => $row) {
+ if ($row->type == $obj_type && $row->revision_id == $vid) {
+ unset($field_data['revisions'][$key]);
+ }
+ }
+ }
+ }
+ }
+
+ foreach ($field_languages as $langcode) {
+ $items = (array) $object->{$field_name}[$langcode];
+ $delta_count = 0;
+ foreach ($items as $delta => $item) {
+ $row = (object) array(
+ 'field_id' => $field_id,
+ 'type' => $obj_type,
+ 'entity_id' => $id,
+ 'revision_id' => $vid,
+ 'bundle' => $bundle,
+ 'delta' => $delta,
+ 'deleted' => FALSE,
+ 'language' => $langcode,
+ );
+ foreach ($field['columns'] as $column => $attributes) {
+ $row->{$column} = isset($item[$column]) ? $item[$column] : NULL;
+ }
+
+ $field_data['current'][] = $row;
+ if (isset($vid)) {
+ $field_data['revisions'][] = $row;
+ }
+
+ if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) {
+ break;
+ }
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implement hook_field_storage_delete().
+ */
+function field_test_field_storage_delete($obj_type, $object, $fields) {
+ list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
+
+ // Note: reusing field_test_storage_purge(), like field_sql_storage.module
+ // does, is highly inefficient in our case...
+ foreach (field_info_instances($bundle) as $instance) {
+ if (isset($fields[$instance['field_id']])) {
+ $field = field_info_field_by_id($instance['field_id']);
+ field_test_field_storage_purge($obj_type, $object, $field, $instance);
+ }
+ }
+}
+
+/**
+ * Implement hook_field_storage_purge().
+ */
+function field_test_field_storage_purge($obj_type, $object, $field, $instance) {
+ $data = _field_test_storage_data();
+
+ list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
+
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as $key => $row) {
+ if ($row->type == $obj_type && $row->entity_id == $id) {
+ unset($field_data[$sub_table][$key]);
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implement hook_field_storage_delete_revision().
+ */
+function field_test_field_storage_delete_revision($obj_type, $object, $fields) {
+ $data = _field_test_storage_data();
+
+ list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
+ foreach ($fields as $field_id) {
+ $field_data = &$data[$field_id];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as $key => $row) {
+ if ($row->type == $obj_type && $row->entity_id == $id && $row->revision_id == $vid) {
+ unset($field_data[$sub_table][$key]);
+ }
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implement hook_field_storage_query().
+ */
+function field_test_field_storage_query($field_id, $conditions, $count, &$cursor = NULL, $age) {
+ $data = _field_test_storage_data();
+
+ $load_current = $age == FIELD_LOAD_CURRENT;
+
+ $field = field_info_field_by_id($field_id);
+ $field_columns = array_keys($field['columns']);
+
+ $field_data = $data[$field['id']];
+ $sub_table = $load_current ? 'current' : 'revisions';
+ // We need to sort records by object type and object id.
+ usort($field_data[$sub_table], '_field_test_field_storage_query_sort_helper');
+
+ // Initialize results array.
+ $return = array();
+ $obj_count = 0;
+ $rows_count = 0;
+ $rows_total = count($field_data[$sub_table]);
+ $skip = $cursor;
+ $skipped = 0;
+
+ foreach ($field_data[$sub_table] as $row) {
+ if ($count != FIELD_QUERY_NO_LIMIT && $obj_count >= $count) {
+ break;
+ }
+
+ if ($row->field_id == $field['id']) {
+ $match = TRUE;
+ $condition_deleted = FALSE;
+ // Add conditions.
+ foreach ($conditions as $condition) {
+ @list($column, $value, $operator) = $condition;
+ if (empty($operator)) {
+ $operator = is_array($value) ? 'IN' : '=';
+ }
+ switch ($operator) {
+ case '=':
+ $match = $match && $row->{$column} == $value;
+ break;
+ case '!=':
+ case '<':
+ case '<=':
+ case '>':
+ case '>=':
+ eval('$match = $match && '. $row->{$column} . ' ' . $operator . ' '. $value);
+ break;
+ case 'IN':
+ $match = $match && in_array($row->{$column}, $value);
+ break;
+ case 'NOT IN':
+ $match = $match && !in_array($row->{$column}, $value);
+ break;
+ case 'BETWEEN':
+ $match = $match && $row->{$column} >= $value[0] && $row->{$column} <= $value[1];
+ break;
+ case 'STARTS_WITH':
+ case 'ENDS_WITH':
+ case 'CONTAINS':
+ // Not supported.
+ $match = FALSE;
+ break;
+ }
+ // Track condition on 'deleted'.
+ if ($column == 'deleted') {
+ $condition_deleted = TRUE;
+ }
+ }
+
+ // Exclude deleted data unless we have a condition on it.
+ if (!$condition_deleted && $row->deleted) {
+ $match = FALSE;
+ }
+
+ if ($match) {
+ if (is_null($skip) || $skipped >= $skip) {
+ $cursor++;
+ // 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['object keys']['revision'])) ? $row->entity_id : $row->revision_id;
+
+ if (!isset($return[$row->type][$id])) {
+ $return[$row->type][$id] = field_create_stub_entity($row->type, array($row->entity_id, $row->revision_id, $row->bundle));
+ $obj_count++;
+ }
+ }
+ else {
+ $skipped++;
+ }
+ }
+ }
+ $rows_count++;
+
+ // The query is complete if we walked the whole array.
+ if ($count != FIELD_QUERY_NO_LIMIT && $rows_count >= $rows_total) {
+ $cursor = FIELD_QUERY_COMPLETE;
+ }
+ }
+
+ return $return;
+}
+
+/**
+ * Sort helper for field_test_field_storage_query().
+ *
+ * Sort by object type and object id.
+ */
+function _field_test_field_storage_query_sort_helper($a, $b) {
+ if ($a->type == $b->type) {
+ if ($a->entity_id == $b->entity_id) {
+ return 0;
+ }
+ else {
+ return $a->entity_id < $b->entity_id ? -1 : 1;
+ }
+ }
+ else {
+ return $a->type < $b->type ? -1 : 1;
+ }
+}
+
+/**
+ * Implement hook_field_storage_create_field().
+ */
+function field_test_field_storage_create_field($field) {
+ if ($field['storage']['type'] == 'field_test_storage_failure') {
+ throw new Exception('field_test_storage_failure engine always fails to create fields');
+ }
+
+ $data = _field_test_storage_data();
+
+ $data[$field['id']] = array(
+ 'current' => array(),
+ 'revisions' => array(),
+ );
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implement hook_field_storage_delete_field().
+ */
+function field_test_field_storage_delete_field($field) {
+ $data = _field_test_storage_data();
+
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as &$row) {
+ $row->deleted = TRUE;
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implement hook_field_storage_delete_instance().
+ */
+function field_test_field_storage_delete_instance($instance) {
+ $data = _field_test_storage_data();
+
+ $field = field_info_field($instance['field_name']);
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as &$row) {
+ if ($row->bundle == $instance['bundle']) {
+ $row->deleted = TRUE;
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implement hook_field_attach_create_bundle().
+ */
+function field_test_field_attach_create_bundle($bundle) {
+ // We don't need to do anything here.
+}
+
+/**
+ * Implement hook_field_attach_rename_bundle().
+ */
+function field_test_field_attach_rename_bundle($bundle_old, $bundle_new) {
+ $data = _field_test_storage_data();
+
+ // We need to account for deleted or inactive fields and instances.
+ $instances = field_read_instances(array('bundle' => $bundle_new), array('include_deleted' => TRUE, 'include_inactive' => TRUE));
+ foreach ($instances as $field_name => $instance) {
+ $field = field_info_field_by_id($instance['field_id']);
+ if ($field['storage']['type'] == 'field_test_storage') {
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as &$row) {
+ if ($row->bundle == $bundle_old) {
+ $row->bundle = $bundle_new;
+ }
+ }
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implement hook_field_attach_delete_bundle().
+ */
+function field_test_field_attach_delete_bundle($bundle, $instances) {
+ $data = _field_test_storage_data();
+
+ $instances = field_info_instances($bundle);
+ foreach ($instances as $field_name => $instance) {
+ $field = field_info_field($field_name);
+ if ($field['storage']['type'] == 'field_test_storage') {
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as &$row) {
+ if ($row->bundle == $bundle_old) {
+ $row->deleted = TRUE;
+ }
+ }
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
* Store and retrieve keyed data for later verification by unit tests.
*
* This function is a simple in-memory key-value store with the
@@ -725,13 +1155,3 @@ function field_test_field_delete($obj_type, $object, $field, $instance, $items)
$args = func_get_args();
field_test_memorize(__FUNCTION__, $args);
}
-
-/**
- *
- * 'Field storage' API.
- *
- */
-
-function field_test_field_storage_create_field($field) {
- throw new Exception('field_test storage module always fails to create fields');
-}