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	11 Feb 2009 07:05:05 -0000
@@ -3980,6 +3980,219 @@ 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 $options
+ *   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, $options, $primary_keys = array(), $unserialize = TRUE) {
+  $records = drupal_load_records($table, $query, $primary_keys, $unserialize);
+  if (!empty($records)) {
+    return $records[0];
+  }
+  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 $options
+ *   An associative array of additional options, 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. If any joins are used, the fields
+ *       must be in the form 'tablename.fieldname'.
+ *     - 'fields'
+ *       An array of names of field to load. If omitted, all fields from the
+ *       base table will be loaded. If any joins are used, the fields must be in
+ *       the form 'tablename.fieldname'.
+ *     - 'joins'
+ *       An array of join information. Each array value must be an array of
+ *       arguments expected by the addJoin() method of a SelectQuery object.
+ *     - '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. If any joins are used, the fields must be in the form
+ *       'tablename.fieldname'.
+ *     - 'alter_tags'
+ *       An array of tags by which the query can be identified for altering. 
+ * @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
+ *   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, $options, $primary_keys = array(), $unserialize = TRUE) {
+
+  foreach (array('conditions', 'fields', 'join', 'order_by', 'alter_tags') as $option) {
+    if (!isset($options[$option])) {
+      $options[$option] = array();
+    }
+  }
+  // Give a default alter tag to identify calls from this API fuction.
+  $options['alter_tags'][] = 'drupal_load_records';
+
+  // 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;
+      }
+    }
+    // It's slightly more efficient to load all fields by name rather than
+    // by the * that will be used if an empty array is fed in.
+    if (empty($query['fields'])) {
+      $query['fields'] = array_keys($schema['fields']);
+    }
+  }
+
+  // Accept a numeric ID key or an array of IDs as conditions.
+  if (is_numeric($query['conditions']) || is_numeric(key($query['conditions']))) {
+    if (count($primary_keys) > 1) {
+      return FALSE;
+    }
+    $options['conditions'] = array($primary_keys[0], $options['conditions']);
+  }
+
+  $query = db_select($table);
+  $query->fields($table, $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);
+  }
+
+  $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 $conditions
+ *   The 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.
+ *   
+ * @return
+ *   Failure to delete based on missing schema information will return FALSE.
+ *   Otherwise SAVED_DELETED.
+ */
+function drupal_delete_records($table, $conditions) {
+  if (is_numeric($conditions) || is_numeric(key($conditions))) {
+    $schema = drupal_get_schema($table);
+    if (empty($schema)) {
+      return FALSE;
+    }
+    if (count($schema['primary keys']) > 1) {
+      return FALSE;
+    }
+    $primary_key = current($schema['primary keys']);
+    $conditions = array($primary_key => $value);
+  }
+  $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	11 Feb 2009 07:05:19 -0000
@@ -783,6 +783,60 @@ 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 = new StdClass();
+    $vocabulary->name = 'test';
+    $insert_result = drupal_write_record('taxonomy_vocabulary', $vocabulary);
+    $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->vid), t('Primary key is set on record created with drupal_write_record().'));
+
+    // Update the initial record after changing a property.
+    $vocabulary->name = 'testing';
+    $update_result = drupal_write_record('taxonomy_vocabulary', $vocabulary, array('vid'));
+    $this->assertTrue($update_result == SAVED_UPDATED, t('Correct value returned when a record updated with drupal_write_record() for table with single-field primary key.'));
+
+    // Insert an object record for a table with a multi-field primary key.
+    $vocabulary_node_type = new StdClass();
+    $vocabulary_node_type->vid = $vocabulary->vid;
+    $vocabulary_node_type->type = 'page';
+    $insert_result = drupal_write_record('taxonomy_vocabulary_node_type', $vocabulary_node_type);
+    $this->assertTrue($insert_result == SAVED_NEW, t('Correct value returned when a record is inserted with drupal_write_record() for a table with a multi-field primary key.'));
+
+    // Update the record.
+    $update_result = drupal_write_record('taxonomy_vocabulary_node_type', $vocabulary_node_type, array('vid', 'type'));
+    $this->assertTrue($update_result == SAVED_UPDATED, t('Correct value returned when a record is updated with drupal_write_record() for a table with a multi-field primary key.'));
+
+    // Test loading a record by primary key.
+    $record = drupal_load_record('vocabulary', array('conditions' => array($vocabulary->vid)));
+    $this->assertTrue($record && $record->name == $vocabulary->name, t('Record loaded by ID via drupal_load_record().'));
+    // Test deleting a record by primary key.
+    $record = drupal_delete_records('vocabulary', array($vocabulary->vid));
+    $record = drupal_load_record('vocabulary', array('conditions' =>  array($vocabulary->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 +907,4 @@ class DrupalErrorCollectionUnitTest exte
     }
   }
 }
+
