Commit 9cf21be9 authored by Dries's avatar Dries

- Patch #780154 by chx, noahb, dhthwy, pwolanin, aspilicious, jhodgdon,...

- Patch #780154 by chx, noahb, dhthwy, pwolanin, aspilicious, jhodgdon, dereine, bjaspan: listing API for field API.
parent f6d56f96
This diff is collapsed.
......@@ -1470,24 +1470,20 @@ function hook_field_storage_delete_revision($entity_type, $entity, $fields) {
}
/**
* Handle a field query.
* Execute an EntityFieldQuery.
*
* This hook is invoked from field_attach_query() to ask the field storage
* module to handle a field query.
* This hook is called to find the entities having certain entity and field
* conditions and sort them in the given field order. If the field storage
* engine also handles property sorts and orders, it should unset those
* properties in the called object to signal that those have been handled.
*
* @param $field_name
* The name of the field to query.
* @param $conditions
* See field_attach_query(). A storage module that doesn't support querying a
* given column should raise a FieldQueryException. Incompatibilities should
* be mentioned on the module project page.
* @param $options
* See field_attach_query(). All option keys are guaranteed to be specified.
* @param EntityFieldQuery $query
* An EntityFieldQuery.
*
* @return
* See field_attach_query().
* See EntityFieldQuery::execute() for the return values.
*/
function hook_field_storage_query($field_name, $conditions, $options) {
function hook_field_storage_query($query) {
// @todo Needs function body
}
......@@ -1657,34 +1653,6 @@ function hook_field_storage_pre_update($entity_type, $entity, &$skip_fields) {
}
}
/**
* Act before the storage backend runs the query.
*
* This hook should be implemented by modules that use
* hook_field_storage_pre_load(), hook_field_storage_pre_insert() and
* hook_field_storage_pre_update() to bypass the regular storage engine, to
* handle field queries.
*
* @param $field_name
* The name of the field to query.
* @param $conditions
* See field_attach_query().
* A storage module that doesn't support querying a given column should raise
* a FieldQueryException. Incompatibilities should be mentioned on the module
* project page.
* @param $options
* See field_attach_query(). All option keys are guaranteed to be specified.
* @param $skip_field
* Boolean, always coming as FALSE.
* @return
* See field_attach_query().
* The $skip_field parameter should be set to TRUE if the query has been
* handled.
*/
function hook_field_storage_pre_query($field_name, $conditions, $options, &$skip_field) {
// @todo Needs function body.
}
/**
* Alters the display settings of a field before it gets displayed.
*
......@@ -1837,8 +1805,12 @@ function hook_field_update_forbid($field, $prior_field, $has_data) {
// Identify the keys that will be lost.
$lost_keys = array_diff(array_keys($field['settings']['allowed_values']), array_keys($prior_field['settings']['allowed_values']));
// If any data exist for those keys, forbid the update.
$count = field_attach_query($prior_field['id'], array('value', $lost_keys, 'IN'), 1);
if ($count > 0) {
$query = new EntityFieldQuery;
$found = $query
->fieldCondition($prior_field['field_name'], 'value', $lost_keys)
->range(0, 1)
->execute();
if ($found) {
throw new FieldUpdateForbiddenException("Cannot update a list field not to include keys with existing data");
}
}
......
......@@ -30,15 +30,6 @@ 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
* @{
......@@ -1045,132 +1036,6 @@ function field_attach_delete_revision($entity_type, $entity) {
module_invoke_all('field_attach_delete_revision', $entity_type, $entity);
}
/**
* Retrieve entities matching a given set of conditions.
*
* Note that the query 'conditions' only apply to the stored values.
* In a regular field_attach_load() call, field values additionally go through
* hook_field_load() and hook_field_attach_load() invocations, which can add
* to or affect the raw stored values. The results of field_attach_query()
* might therefore differ from what could be expected by looking at a regular,
* fully loaded entity.
*
* @param $field_id
* The id of the field to query.
* @param $conditions
* An array of query conditions. Each condition is a numerically indexed
* array, in the form: array(column, value, operator).
* Not all storage engines are required to support queries on all column, or
* with all operators below. A FieldQueryException will be raised if an
* unsupported condition is specified.
* Supported columns:
* - any of the columns defined in hook_field_schema() for $field_name's
* field type: condition on field value,
* - 'type': condition on entity type (e.g. 'node', 'user'...),
* - 'bundle': condition on entity bundle (e.g. node type),
* - 'entity_id': condition on entity id (e.g node nid, user uid...),
* - 'deleted': condition on whether the field's data is
* marked deleted for the entity (defaults to FALSE if not specified)
* The field_attach_query_revisions() function additionally supports:
* - 'revision_id': condition on entity revision id (e.g node vid).
* Supported operators:
* - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'ENDS_WITH',
* 'CONTAINS': these operators expect the value as a literal of the same
* type as the column,
* - 'IN', 'NOT IN': this operator expects the value as an array of
* literals of the same type as the column.
* - 'BETWEEN': this operator expects the value as an array of two literals
* of the same type as the column.
* The operator can be ommitted, and will default to 'IN' if the value is
* an array, or to '=' otherwise.
* Example values for $conditions:
* @code
* array(
* array('type', 'node'),
* );
* array(
* array('bundle', array('article', 'page')),
* array('value', 12, '>'),
* );
* @endcode
* @param $options
* An associative array of additional options:
* - limit: The number of results that is requested. This is only a hint to
* the storage engine(s); callers should be prepared to handle fewer or
* more results. Specify FIELD_QUERY_NO_LIMIT to retrieve all available
* entities. This option has a default value of 0 so callers must make an
* explicit choice to potentially retrieve an enormous result set.
* - cursor: A reference to an opaque cursor that allows a caller to iterate
* through multiple result sets. On the first call, pass 0; the correct
* value to pass on the next call will be written into the value on return.
* When there is no more query data available, the value will be filled in
* with FIELD_QUERY_COMPLETE. If cursor is passed as NULL, the first result
* set is returned and no next cursor is returned.
* - count: If TRUE, return a single count of all matching entities; limit and
* cursor are ignored.
* - age: Internal use only. Use field_attach_query_revisions() instead of
* passing FIELD_LOAD_REVISION.
* - FIELD_LOAD_CURRENT (default): query the most recent revisions for all
* entities. The results will be keyed by entity type and entity id.
* - FIELD_LOAD_REVISION: query all revisions. The results will be keyed by
* entity type and entity revision id.
* @return
* An array keyed by entity type (e.g. 'node', 'user'...), then by entity id
* or revision id (depending of the value of the $age parameter). The values
* are pseudo-entities with the bundle, id, and revision id fields filled in.
* Throws a FieldQueryException if the field's storage doesn't support the
* specified conditions.
*/
function field_attach_query($field_id, $conditions, $options = array()) {
// Merge in default options.
$default_options = array(
'limit' => 0,
'cursor' => 0,
'count' => FALSE,
'age' => FIELD_LOAD_CURRENT,
);
$options += $default_options;
// Give a chance to 3rd party modules that bypass the storage engine to
// handle the query.
$skip_field = FALSE;
foreach (module_implements('field_storage_pre_query') as $module) {
$function = $module . '_field_storage_pre_query';
$results = $function($field_id, $conditions, $options, $skip_field);
// Stop as soon as a module claims it handled the query.
if ($skip_field) {
break;
}
}
// If the request hasn't been handled, let the storage engine handle it.
if (!$skip_field) {
$field = field_info_field_by_id($field_id);
$function = $field['storage']['module'] . '_field_storage_query';
$results = $function($field_id, $conditions, $options);
}
return $results;
}
/**
* Retrieve entity revisions matching a given set of conditions.
*
* See field_attach_query() for more informations.
*
* @param $field_id
* The id of the field to query.
* @param $conditions
* See field_attach_query().
* @param $options
* An associative array of additional options. See field_attach_query().
* @return
* See field_attach_query().
*/
function field_attach_query_revisions($field_id, $conditions, $options = array()) {
$options['age'] = FIELD_LOAD_REVISION;
return field_attach_query($field_id, $conditions, $options);
}
/**
* Prepare field data prior to display.
*
......
......@@ -1036,25 +1036,23 @@ function field_purge_batch($batch_size) {
$instances = field_read_instances(array('deleted' => 1), array('include_deleted' => 1));
foreach ($instances as $instance) {
// field_purge_data() will need the field array.
$field = field_info_field_by_id($instance['field_id']);
// Retrieve some entities.
$query = new EntityFieldQuery;
$results = $query
->fieldCondition($field)
->entityCondition('bundle', $instance['bundle'])
->deleted(TRUE)
->range(0, $batch_size)
->execute();
// Retrieve some pseudo-entities.
$entity_types = field_attach_query($instance['field_id'], array(array('bundle', $instance['bundle']), array('deleted', 1)), array('limit' => $batch_size));
if (count($entity_types) > 0) {
// Field data for the instance still exists.
foreach ($entity_types as $entity_type => $entities) {
field_attach_load($entity_type, $entities, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1));
foreach ($entities as $id => $entity) {
// field_attach_query() may return more results than we asked for.
// Stop when he have done our batch size.
if ($batch_size-- <= 0) {
return;
}
if ($results) {
foreach ($results as $entity_type => $stub_entities) {
field_attach_load($entity_type, $stub_entities, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1));
foreach ($stub_entities as $stub_entity) {
// Purge the data for the entity.
field_purge_data($entity_type, $entity, $field, $instance);
field_purge_data($entity_type, $stub_entity, $field, $instance);
}
}
}
......@@ -1164,4 +1162,3 @@ function field_purge_field($field) {
/**
* @} End of "defgroup field_purge".
*/
......@@ -101,28 +101,6 @@ class FieldException extends Exception {}
*/
define('FIELD_LOAD_REVISION', 'FIELD_LOAD_REVISION');
/**
* @name Field query flags
* @{
* Flags for field_attach_query().
*/
/**
* Limit argument for field_attach_query() to request all available
* entities instead of a limited number.
*/
define('FIELD_QUERY_NO_LIMIT', 'FIELD_QUERY_NO_LIMIT');
/**
* Cursor return value for field_attach_query() to indicate that no
* more data is available.
*/
define('FIELD_QUERY_COMPLETE', 'FIELD_QUERY_COMPLETE');
/**
* @} End of "Field query flags".
*/
/**
* Exception class thrown by hook_field_update_forbid().
*/
......@@ -877,8 +855,12 @@ function field_get_items($entity_type, $entity, $field_name, $langcode = NULL) {
* TRUE if the field has data for any entity; FALSE otherwise.
*/
function field_has_data($field) {
$results = field_attach_query($field['id'], array(), array('limit' => 1));
return !empty($results);
$query = new EntityFieldQuery();
return (bool) $query
->fieldCondition($field)
->range(0, 1)
->count()
->execute();
}
/**
......
......@@ -482,127 +482,114 @@ function field_sql_storage_field_storage_purge($entity_type, $entity, $field, $i
/**
* Implements hook_field_storage_query().
*/
function field_sql_storage_field_storage_query($field_id, $conditions, $options) {
$load_current = $options['age'] == FIELD_LOAD_CURRENT;
$field = field_info_field_by_id($field_id);
$field_name = $field['field_name'];
$table = $load_current ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field);
$field_columns = array_keys($field['columns']);
// Build the query.
$query = db_select($table, 't');
$query->join('field_config_entity_type', 'e', 't.etid = e.etid');
// Add conditions.
foreach ($conditions as $condition) {
// A condition is either a (column, value, operator) triple, or a
// (column, value) pair with implied operator.
@list($column, $value, $operator) = $condition;
// Translate operator and value if needed.
switch ($operator) {
case 'STARTS_WITH':
$operator = 'LIKE';
$value = db_like($value) . '%';
break;
case 'ENDS_WITH':
$operator = 'LIKE';
$value = '%' . db_like($value);
break;
case 'CONTAINS':
$operator = 'LIKE';
$value = '%' . db_like($value) . '%';
break;
function field_sql_storage_field_storage_query(EntityFieldQuery $query) {
$groups = array();
if ($query->age == FIELD_LOAD_CURRENT) {
$tablename_function = '_field_sql_storage_tablename';
$id_key = 'entity_id';
}
else {
$tablename_function = '_field_sql_storage_revision_tablename';
$id_key = 'revision_id';
}
$table_aliases = array();
// Add tables for the fields used.
foreach ($query->fields as $key => $field) {
$tablename = $tablename_function($field);
// Every field needs a new table.
$table_alias = $tablename . $key;
$table_aliases[$key] = $table_alias;
if ($key) {
$select_query->join($tablename, $table_alias, "$table_alias.etid = $field_base_table.etid AND $table_alias.$id_key = $field_base_table.$id_key");
}
else {
$select_query = db_select($tablename, $table_alias);
$select_query->fields($table_alias, array('entity_id', 'revision_id', 'bundle'));
// As only a numeric ID is stored instead of the entity type add the
// field_config_entity_type table to resolve the etid to a more readable
// name.
$select_query->join('field_config_entity_type', 'fcet', "fcet.etid = $table_alias.etid");
$select_query->addField('fcet', 'type', 'entity_type');
$field_base_table = $table_alias;
}
// Translate field columns into prefixed db columns.
if (in_array($column, $field_columns)) {
$column = _field_sql_storage_columnname($field_name, $column);
if ($field['cardinality'] != 1) {
$select_query->distinct();
}
// Translate entity types into numeric ids. Expressing the condition on the
// local 'etid' column rather than the JOINed 'type' column avoids a
// filesort.
if ($column == 'type') {
$column = 't.etid';
if (is_array($value)) {
foreach (array_keys($value) as $key) {
$value[$key] = _field_sql_storage_etid($value[$key]);
}
// Add field conditions.
foreach ($query->fieldConditions as $key => $condition) {
$table_alias = $table_aliases[$key];
$field = $condition['field'];
// Add the specified condition.
$sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $condition['column']);
$query->addCondition($select_query, $sql_field, $condition);
// Add delta / language group conditions.
foreach (array('delta', 'language') as $column) {
if (isset($condition[$column . '_group'])) {
$group_name = $condition[$column . '_group'];
if (!isset($groups[$column][$group_name])) {
$groups[$column][$group_name] = $table_alias;
}
else {
$select_query->where("$table_alias.$column = " . $groups[$column][$group_name] . ".$column");
}
}
else {
$value = _field_sql_storage_etid($value);
}
}
// Track condition on 'deleted'.
if ($column == 'deleted') {
$condition_deleted = TRUE;
}
$query->condition($column, $value, $operator);
}
// Exclude deleted data unless we have a condition on it.
if (!isset($condition_deleted)) {
$query->condition('deleted', 0);
// Add field orders.
foreach ($query->fieldOrder as $key => $order) {
$table_alias = $table_aliases[$key];
$field = $order['field'];
$sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $order['column']);
$select_query->orderBy($sql_field, $order['direction']);
}
// For a count query, return the count now.
if ($options['count']) {
return $query
->fields('t', array('etid', 'entity_id', 'revision_id'))
->distinct()
->countQuery()
->execute()
->fetchField();
if (isset($query->deleted)) {
$select_query->condition("$field_base_table.deleted", (int) $query->deleted);
}
// For a data query, add fields.
$query
->fields('t', array('bundle', 'entity_id', 'revision_id'))
->fields('e', array('type'))
// We need to ensure entities arrive in a consistent order for the
// range() operation to work.
->orderBy('t.etid')
->orderBy('t.entity_id');
// Initialize results array
$return = array();
// Getting $count entities possibly requires reading more than $count rows
// since fields with multiple values span over several rows. We query for
// batches of $count rows until we've either read $count entities or received
// less rows than asked for.
$entity_count = 0;
do {
if ($options['limit'] != FIELD_QUERY_NO_LIMIT) {
$query->range($options['cursor'], $options['limit']);
}
$results = $query->execute();
$row_count = 0;
foreach ($results as $row) {
$row_count++;
$options['cursor']++;
// If querying all revisions and the entity type has revisions, we need
// to key the results by revision_ids.
$entity_type = entity_get_info($row->type);
$id = ($load_current || empty($entity_type['entity keys']['revision'])) ? $row->entity_id : $row->revision_id;
if (!isset($return[$row->type][$id])) {
$return[$row->type][$id] = entity_create_stub_entity($row->type, array($row->entity_id, $row->revision_id, $row->bundle));
$entity_count++;
}
if ($query->propertyConditions || $query->propertyOrder) {
if (empty($query->entityConditions['entity_type']['value'])) {
throw new EntityFieldQueryException('Property conditions and orders must have an entity type defined.');
}
} while ($options['limit'] != FIELD_QUERY_NO_LIMIT && $row_count == $options['limit'] && $entity_count < $options['limit']);
// The query is complete when the last batch returns less rows than asked
// for.
if ($row_count < $options['limit']) {
$options['cursor'] = FIELD_QUERY_COMPLETE;
$entity_type = $query->entityConditions['entity_type']['value'];
$entity_base_table = _field_sql_storage_query_join_entity($select_query, $entity_type, $field_base_table);
$query->entityConditions['entity_type']['operator'] = '=';
$query->processProperty($select_query, $entity_base_table);
}
foreach ($query->entityConditions as $key => $condition) {
$sql_field = $key == 'entity_type' ? 'fcet.type' : "$field_base_table.$key";
$query->addCondition($select_query, $sql_field, $condition);
}
foreach ($query->entityOrder as $key => $direction) {
$sql_field = $key == 'entity_type' ? 'fcet.type' : "$field_base_table.$key";
$query->orderBy($sql_field, $direction);
}
return $query->finishQuery($select_query, $id_key);
}
return $return;
/**
* Adds the base entity table to a field query object.
*
* @param SelectQuery $select_query
* A SelectQuery containing at least one table as specified by
* _field_sql_storage_tablename().
* @param $entity_type
* The entity type for which the base table should be joined.
* @param $field_base_table
* Name of a table in $select_query. As only INNER JOINs are used, it does
* not matter which.
*
* @return
* The name of the entity base table joined in.
*/
function _field_sql_storage_query_join_entity(SelectQuery $select_query, $entity_type, $field_base_table) {
$entity_info = entity_get_info($entity_type);
$entity_base_table = $entity_info['base table'];
$entity_field = $entity_info['entity keys']['id'];
$select_query->join($entity_base_table, $entity_base_table, "$entity_base_table.$entity_field = $field_base_table.entity_id");
return $entity_base_table;
}
/**
......
......@@ -99,7 +99,7 @@ function list_field_schema($field) {
*
* @todo: If $has_data, add a form validate function to verify that the
* new allowed values do not exclude any keys for which data already
* exists in the databae (use field_attach_query()) to find out.
* exists in the field storage (use EntityFieldQuery to find out).
* Implement the validate function via hook_field_update_forbid() so
* list.module does not depend on form submission.
*/
......
This diff is collapsed.
......@@ -27,6 +27,8 @@ function field_test_entity_info() {
'name' => t('Test Entity'),
'fieldable' => TRUE,
'field cache' => FALSE,
'base table' => 'test_entity',
'revision table' => 'test_entity_revision',
'entity keys' => array(
'id' => 'ftid',
'revision' => 'ftvid',
......@@ -48,6 +50,29 @@ function field_test_entity_info() {
'bundles' => $bundles,
'view modes' => $test_entity_modes,
),
'test_entity_bundle_key' => array(
'name' => t('Test Entity with a bundle key.'),
'base table' => 'test_entity_bundle_key',
'fieldable' => TRUE,
'field cache' => FALSE,
'entity keys' => array(
'id' => 'ftid',
'bundle' => 'fttype',
),
'bundles' => array('bundle1' => array('label' => 'Bundle1'), 'bundle2' => array('label' => 'Bundle2')),
'view modes' => $test_entity_modes,
),
'test_entity_bundle' => array(
'name' => t('Test Entity with a specified bundle.'),
'base table' => 'test_entity_bundle',
'fieldable' => TRUE,
'field cache' => FALSE,
'entity keys' => array(
'id' => 'ftid',
),
'bundles' => array('test_entity_2' => array('label' => 'Test entity 2')),
'view modes' => $test_entity_modes,
),
);
}
......
......@@ -26,6 +26,14 @@ function field_test_field_info() {
'default_widget' => 'test_field_widget',
'default_formatter' => 'field_test_default',
),
'shape' => array(
'label' => t('Shape'),
'description' => t('Another dummy field type.'),
'settings' => array(),
'instance_settings' => array(),
'default_widget' => 'test_field_widget',
'default_formatter' => 'field_test_default',
),
'hidden_test_field' => array(
'no_ui' => TRUE,
'label' => t('Hidden from UI test field'),
......@@ -42,18 +50,36 @@ function field_test_field_info() {
* Implements hook_field_schema().
*/
function field_test_field_schema($field) {
return array(
'columns' => array(
'value' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => FALSE,
if ($field['type'] == 'test_field') {
return array(
'columns' => array(
'value' => array(
'type' => 'int',