Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.865 diff -u -p -r1.865 common.inc --- includes/common.inc 9 Feb 2009 03:29:53 -0000 1.865 +++ includes/common.inc 12 Feb 2009 17:06:03 -0000 @@ -3980,6 +3980,251 @@ function drupal_write_record($table, &$o } /** + * Load a record from the database, consulting the schema if necessary. + * + * @param $table + * The name of the table; this must exist in schema API. + * @param $query + * A SelectQuery object or an associative array of additional options with the + * structure given in drupal_load_records(). + * @param $primary_keys + * An array of primary keys of the base table. If empty, the primary keys will + * be determined by consulting the schema. + * @param $unserialize + * If TRUE, the schema will be consulted and all fields with a 'serialize' + * value of TRUE will be unserialized. If an array of field names is given, + * these fields will be unserialized in the results. If FALSE, + * @return + * A matching record, or FALSE on failure. + */ +function drupal_load_record($table, $query, $primary_keys = array(), $unserialize = TRUE) { + $records = drupal_load_records($table, $query, $primary_keys, $unserialize); + if (!empty($records)) { + return current($records); + } + else { + return FALSE; + } +} + +/** + * Load one or more records from the database, consulting the schema if necessary. + * + * This method is a generic loader that can be used with any type of record set + * that needs to be loaded from one or more tables. Because queries are given an + * alter tag of 'drupal_write_records', it is possible to alter any query to, + * e.g., add joins on one or more other tables. Custom alter tags may also be + * given to increase the specificity of altering. + * + * Fields holding serialized data may be requested in unserialized form. + * + * If called with one or two arguments, the function will consult the schema to + * determine primary key fields and fields to be unserialized. To prevent schema + * loading, feed an array of $primary_key values and set $unserialize to either + * FALSE or an array of field names to be unserialized. + * + * @param $table + * The name of the base table to load records from; this must exist in schema + * API. + * @param $query + * This argument can be used in a number of different ways. First, a + * SelectQuery object is supported. Alternately, one or a an array of primary + * key integer values may be fed. Finally, an associative array of field + * names-value pairs and additional option properties is supported. Field + * values may themselves be in array form, in which case the 'IN' operator + * will be used. In addition to field-value pairs, these additional properties + * are supported: + * - '#conditions' + * An array of conditions to apply to the query. + * - '#fields' + * An array of names of field to load. If omitted, all fields from the + * base table will be loaded. + * - '#range' + * An array of arguments in the form expected by the addRange() method of + * a SelectQuery object. The first value is the 'start' and the second the + * 'length' of the range of records to load. + * - '#distinct' + * Boolean: whether the query should be flagged as DISTINCT. + * - '#order_by' + * An array of order by clauses, each in the form of arguments expected by + * the orderBy() method of the SelectQuery object: the field name and the + * direction. + * - '#alter_tags' + * An array of tags by which the query can be identified for altering. + * @param $primary_keys + * A single primary key field name or array of primary keys of the base table. + * If empty, the primary keys will be determined by consulting the schema. + * @param $unserialize + * If TRUE, the schema will be consulted and all fields with a 'serialize' + * value of TRUE will be unserialized. If an array of field names is given, + * these fields will be unserialized in the results. If FALSE, + * @return + * An array of all matching records, or FALSE on failure. If there is a single + * primary key field, the array of results is keyed by primary key value. + */ +function drupal_load_records($table, $query, $primary_keys = array(), $unserialize = FALSE, $alter_results = FALSE) { + // Standardize $primary_keys to an array. + if (is_string($primary_keys)) { + $primary_keys = array($primary_keys); + } + + // If we don't have primary keys, or need to unserialize but don't have + // a list of fields, we need to load the schema. + if (empty($primary_keys) || $unserialize === TRUE) { + $schema = drupal_get_schema($table); + if (empty($schema)) { + return array(); + } + $primary_keys = $schema['primary key']; + $unserialize = array(); + foreach ($schema['fields'] as $field_name => $field_data) { + if (!empty($field_data['serialize'])) { + $unserialize[] = $field_name; + } + } + } + + // If this is not already a SelectQuery, read in the options and construct + // a query. + if (!($query instanceOf SelectQuery)) { + $options = $query; + // Accept a numeric ID key or an array of IDs as conditions. + if (is_numeric($options) || is_numeric(key($options))) { + if (count($primary_keys) > 1) { + return FALSE; + } + // Need to reset $options to ensure we don't still have element_children. + $options = array( + '#conditions' => array( + array( + current($primary_keys), $options, is_array($options) ? 'IN' : '=', + ) + ) + ); + } + + foreach (array('#conditions', '#fields', '#joins', '#order_by', '#alter_tags') as $option) { + if (!isset($options[$option])) { + $options[$option] = array(); + } + } + + // Accept field-value pairs. + foreach (element_children($options) as $field) { + $options['#conditions'][] = array($field, $options[$field], is_array($options[$field]) ? 'IN' : '='); + } + $query = db_select($table); + // If $options['fields'] is an empty array, all fields will be used. + $query->fields($table, $options['#fields']); + foreach ($options['#conditions'] as $condition) { + list($field, $value, $operator) = $condition; + $query->condition($field, $value, $operator); + } + foreach ($options['#joins'] as $join) { + list($type, $table, $alias, $condition, $arguments) = $join; + $query->addJoin($type, $table, $alias, $condition, $arguments); + } + if (isset($options['#range'])) { + list($start, $length) = $options['#range']; + $query->range($start, $length); + } + if (isset($options['#distinct'])) { + $query->distinct($options['#distinct']); + } + foreach ($options['#order_by'] as $order_by) { + list($field, $direction) = $order_by; + $query->orderBy($field, $direction); + } + foreach ($options['#alter_tags'] as $tag) { + $query->addTag($tag); + } + } + + // Give a default alter tag to identify calls from this API fuction. + $query->addTag('drupal_load_records'); + + $result = $query->execute(); + // Key results by primary key if there is a single-field primary key. + if (count($primary_keys) == 1) { + $result = $result->fetchAllAssoc(current($primary_keys)); + } + else { + $result = $result->fetchAll(); + } + if (empty($result)) { + return array(); + } + + if ($unserialize) { + foreach (array_keys($result) as $key) { + // Iterate through result records. + foreach ($result[$key] as $field => $value) { + // If required, unserialize results. + if (in_array($field, $unserialize)) { + $result[$key]->$field = unserialize($value); + } + } + } + } + + return $result; +} + +/** + * Delete one or more records from the database, consulting the schema if + * necessary. + * + * @param $table + * The name of the table. + * @param $query + * A DeleteQuery object, or a set of conditions to match for deletion. If an + * integer or array of integers is given, these are treated as primary key + * values with the primary key being determined from the schema. Matching + * criteria may also be fed as an array of key-value pairs keyed by field name, + * in which case the schema is not consulted. + * @param $primary_keys + * A single primary key field name or array of primary keys of the base table. + * If empty, the primary keys will be determined if needed by consulting the + * schema. + * + * @return + * Failure to delete based on missing schema information will return FALSE. + * Otherwise SAVED_DELETED. + */ +function drupal_delete_records($table, $query, $primary_keys = array()) { + // Standardize $primary_keys to an array. + if (is_string($primary_keys)) { + $primary_keys = array($primary_keys); + } + + // If this is not already a DeleteQuery, read in the conditions and construct + // a query. + if (!($query instanceOf SelectQuery)) { + $conditions = $query; + if (is_numeric($conditions) || is_numeric(key($conditions))) { + // Consult the schema only if we don't already know the primary keys. + if (empty($primary_keys)) { + $schema = drupal_get_schema($table); + if (empty($schema)) { + return FALSE; + } + if (count($schema['primary key']) > 1) { + return FALSE; + } + $primary_keys = $schema['primary key']; + } + + $conditions = array(current($primary_keys) => $conditions); + } + $query = db_delete($table); + foreach ($conditions as $field => $value) { + $query->condition($field, $value, is_array($value) ? 'IN' : '='); + } + } + $query->execute(); +} + +/** * @} End of "ingroup schemaapi". */ Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.26 diff -u -p -r1.26 common.test --- modules/simpletest/tests/common.test 9 Feb 2009 03:29:54 -0000 1.26 +++ modules/simpletest/tests/common.test 12 Feb 2009 17:06:17 -0000 @@ -783,6 +783,89 @@ class ValidUrlTestCase extends DrupalWeb } /** + * Tests for CRUD API functions. + */ +class DrupalDataApiTest extends DrupalWebTestCase { + function getInfo() { + return array( + 'name' => t('Data API functions'), + 'description' => t('Tests the performance of CRUD APIs.'), + 'group' => t('System'), + ); + } + + function setUp() { + parent::setUp('taxonomy'); + } + + /** + * Test data API methods. + */ + function testDrupalDataApis() { + // Insert an object record for a table with a single-field primary key. + $vocabulary_1 = new StdClass(); + $vocabulary_1->name = 'test'; + $insert_result = drupal_write_record('taxonomy_vocabulary', $vocabulary_1); + // Insert a second record for use below. + $vocabulary_2 = new StdClass(); + $vocabulary_2->name = 'test 2'; + drupal_write_record('taxonomy_vocabulary', $vocabulary_2); + $this->assertTrue($insert_result == SAVED_NEW, t('Correct value returned when a record is inserted with drupal_write_record() for a table with a single-field primary key.')); + $this->assertTrue(isset($vocabulary_1->vid), t('Primary key is set on record created with drupal_write_record().')); + + // Update the initial record after changing a property. + $vocabulary_1->name = 'testing'; + $update_result = drupal_write_record('taxonomy_vocabulary', $vocabulary_1, array('vid')); + + // Insert an object record for a table with a multi-field primary key. + $vocabulary_1_node_type = new StdClass(); + $vocabulary_1_node_type->vid = $vocabulary_1->vid; + $vocabulary_1_node_type->type = 'page'; + $insert_result = drupal_write_record('taxonomy_vocabulary_node_type', $vocabulary_1_node_type); + + // Update the record. + $update_result = drupal_write_record('taxonomy_vocabulary_node_type', $vocabulary_1_node_type, array('vid', 'type')); + + // Test loading a single record by primary key. + $record = drupal_load_record('taxonomy_vocabulary', $vocabulary_1->vid); + $this->assertTrue($record && $record->name == $vocabulary_1->name, t('Record loaded by ID via drupal_load_record().')); + + // Test loading multiple records by primary key. + $records = drupal_load_records('taxonomy_vocabulary', array($vocabulary_1->vid, $vocabulary_2->vid)); + $this->assertTrue(count($records) == 2, t('Multiple records loaded by ID via drupal_load_records().')); + $this->assertTrue(array_keys($records) == array($vocabulary_1->vid, $vocabulary_2->vid), t('Results keyed by primary key for a single-field primary key table loaded with drupal_load_records().')); + + // Test loading multiple records with a complex set of options. + $options = array( + // Keys without a leading # are treated as field-value pairs. + 'name' => array('testing', 'test 2'), + '#fields' => array( + 'vid', + 'name', + ), + '#order_by' => array( + array('name', 'DESC'), + ), + ); + $records = drupal_load_records('taxonomy_vocabulary', $options); + $this->assertTrue(count($records) == 2, t('Multiple records loaded by complex options via drupal_load_records().')); + + // Test loading multiple records passing a SelectQuery object. + $query = db_select('taxonomy_vocabulary') + ->fields('taxonomy_vocabulary') + ->condition('vid', array($vocabulary_1->vid, $vocabulary_2->vid), 'IN'); + $records = drupal_load_records('taxonomy_vocabulary', $query); + $this->assertTrue(count($records) == 2, t('Multiple records loaded by passing a SelectQuery object via drupal_load_records().')); + + // Test deleting a record by primary key. + $record = drupal_delete_records('taxonomy_vocabulary', array($vocabulary_1->vid)); + $record = drupal_load_record('vocabulary', $vocabulary_1->vid, 'vid'); + $this->assertTrue($record === FALSE, t('Record deleted by ID via drupal_delete_records().')); + } + +} + +/** * Tests Simpletest error and exception collecter. */ class DrupalErrorCollectionUnitTest extends DrupalWebTestCase { @@ -853,3 +936,4 @@ class DrupalErrorCollectionUnitTest exte } } } +