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 03:03:13 -0000
@@ -3980,6 +3980,245 @@ 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 additional
+ *   options is supported, with the following keys:
+ *     - 'conditions'
+ *       An array of conditions to apply to the query. If an integer or array
+ *       of integers is given, these are treated as primary key values.
+ *       Conditions may also be fed as an associative array of field names and
+ *       values to match by. Values may themselves be in array form, in which
+ *       case the 'IN' operator will be used.
+ *     - '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;
+      }
+      $options = array('conditions' => array($primary_keys[0] => $options));
+    }
+    foreach (array('conditions', 'fields', 'joins', 'order_by', 'alter_tags') as $option) {
+      if (!isset($options[$option])) {
+        $options[$option] = array();
+      }
+    }
+    // Accept a numeric ID key or an array of IDs as conditions.
+    if (is_numeric($options['conditions']) || is_numeric(key($options['conditions']))) {
+      if (count($primary_keys) > 1) {
+        return FALSE;
+      }
+      $options['conditions'] = array(current($primary_keys) => $options['conditions']);
+    }
+    $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 $field => $value) {
+      $query->condition($field, $value, is_array($value) ? 'IN' : '=');
+    }
+    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($primary_keys[0]);
+  }
+  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 03:03:26 -0000
@@ -783,6 +783,90 @@ 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(
+      'conditions' => array(
+        '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 +937,4 @@ class DrupalErrorCollectionUnitTest exte
     }
   }
 }
+
