From e07b9d35a1f4dcb1678c4d3bb6482daaebea6350 Mon Sep 17 00:00:00 2001
From: Angie Byron <webchick@24967.no-reply.drupal.org>
Date: Fri, 8 Jan 2010 06:07:03 +0000
Subject: [PATCH] #227677 by c960657, yched, cha0s, Dave Reid, et al: Fixed
 drupal_write_record() can't update a column to NULL. (with tests)

---
 includes/common.inc                  | 89 ++++++++++++++--------------
 modules/simpletest/tests/common.test | 80 +++++++++++++++++++++----
 2 files changed, 112 insertions(+), 57 deletions(-)

diff --git a/includes/common.inc b/includes/common.inc
index 6cf0fc1791e3..080caab1bed6 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -5773,19 +5773,18 @@ function drupal_schema_fields_sql($table, $prefix = NULL) {
  * @param $table
  *   The name of the table; this must exist in schema API.
  * @param $object
- *   The object to write. This is a reference, as defaults according to
- *   the schema may be filled in on the object, as well as ID on the serial
- *   type(s). Both array and object types may be passed.
+ *   The object to write. This is a reference, as defaults according to the
+ *   schema may be filled in on the object, as well as ID on the serial type(s).
+ *   Both array and object types may be passed.
  * @param $primary_keys
  *   If this is an update, specify the primary keys' field names. It is the
- *   caller's responsibility to know if a record for this object already
- *   exists in the database. If there is only 1 key, you may pass a simple string.
+ *   caller's responsibility to know if a record for this object already exists
+ *   in the database. If there is only 1 key, you may pass a simple string.
  * @return
  *   Failure to write a record will return FALSE. Otherwise SAVED_NEW or
- *   SAVED_UPDATED is returned depending on the operation performed. The
- *   $object parameter contains values for any serial fields defined by
- *   the $table. For example, $object->nid will be populated after inserting
- *   a new node.
+ *   SAVED_UPDATED is returned depending on the operation performed. The $object
+ *   parameter contains values for any serial fields defined by the $table. For
+ *   example, $object->nid will be populated after inserting a new a new node.
  */
 function drupal_write_record($table, &$object, $primary_keys = array()) {
   // Standardize $primary_keys to an array.
@@ -5812,55 +5811,53 @@ function drupal_write_record($table, &$object, $primary_keys = array()) {
   // Go through our schema, build SQL, and when inserting, fill in defaults for
   // fields that are not set.
   foreach ($schema['fields'] as $field => $info) {
-    // Special case -- skip serial types if we are updating.
-    if ($info['type'] == 'serial' && !empty($primary_keys)) {
-      continue;
+    if ($info['type'] == 'serial') {
+      // Skip serial types if we are updating.
+      if (!empty($primary_keys)) {
+        continue;
+      }
+      // Track serial field so we can helpfully populate them after the query.
+      // NOTE: Each table should come with one serial field only.
+      $serial = $field;
     }
 
-    // For inserts, populate defaults from schema if not already provided.
-    if (!isset($object->$field) && empty($primary_keys) && isset($info['default'])) {
+    if (!property_exists($object, $field)) {
+      // Skip fields that are not provided, unless we are inserting and a
+      // default value is provided by the schema.
+      if (!empty($primary_keys) || !isset($info['default'])) {
+        continue;
+      }
       $object->$field = $info['default'];
     }
 
-    // Track serial field so we can helpfully populate them after the query.
-    // NOTE: Each table should come with one serial field only.
-    if ($info['type'] == 'serial') {
-      $serial = $field;
+    // Build array of fields to update or insert.
+    if (empty($info['serialize'])) {
+      $fields[$field] = $object->$field;
+    }
+    elseif (!empty($object->$field)) {
+      $fields[$field] = serialize($object->$field);
+    }
+    else {
+      $fields[$field] = '';
     }
 
-    // Build arrays for the fields and values in our query.
-    if (isset($object->$field)) {
-      if (empty($info['serialize'])) {
-        $fields[$field] = $object->$field;
+    // Type cast to proper datatype, except when the value is NULL and the
+    // column allows this.
+    //
+    // MySQL PDO silently casts e.g. FALSE and '' to 0 when inserting the value
+    // into an integer column, but PostgreSQL PDO does not. Also type cast NULL
+    // when the column does not allow this.
+    if (!is_null($object->$field) || !empty($info['not null'])) {
+      if ($info['type'] == 'int' || $info['type'] == 'serial') {
+        $fields[$field] = (int) $fields[$field];
       }
-      elseif (!empty($object->$field)) {
-        $fields[$field] = serialize($object->$field);
+      elseif ($info['type'] == 'float') {
+        $fields[$field] = (float) $fields[$field];
       }
       else {
-        $fields[$field] = '';
+        $fields[$field] = (string) $fields[$field];
       }
     }
-
-    // We don't need to care about type casting if value does not exist.
-    if (!isset($fields[$field])) {
-      continue;
-    }
-
-    // Special case -- skip null value if field allows null.
-    if ($fields[$field] == NULL && $info['not null'] == FALSE) {
-      continue;
-    }
-
-    // Type cast if field does not allow null. Required by DB API.
-    if ($info['type'] == 'int' || $info['type'] == 'serial') {
-      $fields[$field] = (int) $fields[$field];
-    }
-    elseif ($info['type'] == 'float') {
-      $fields[$field] = (float) $fields[$field];
-    }
-    else {
-      $fields[$field] = (string) $fields[$field];
-    }
   }
 
   if (empty($fields)) {
diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test
index e1a45dad2948..156bb8fd8b10 100644
--- a/modules/simpletest/tests/common.test
+++ b/modules/simpletest/tests/common.test
@@ -1460,29 +1460,87 @@ class DrupalDataApiTest extends DrupalWebTestCase {
   }
 
   function setUp() {
-    parent::setUp('taxonomy');
+    parent::setUp('database_test');
   }
 
   /**
    * Test the drupal_write_record() API function.
    */
   function testDrupalWriteRecord() {
-    // 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);
+    // Insert a record - no columns allow NULL values.
+    $person = new stdClass();
+    $person->name = 'John';
+    $person->unknown_column = 123;
+    $insert_result = drupal_write_record('test', $person);
     $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(isset($person->id), t('Primary key is set on record created with drupal_write_record().'));
+    $this->assertIdentical($person->age, 0, t('Age field set to default value.'));
+    $this->assertIdentical($person->job, 'Undefined', t('Job field set to default value.'));
+
+    // Verify that the record was inserted.
+    $result = db_query("SELECT * FROM {test} WHERE id = :id", array(':id' => $person->id))->fetchObject();
+    $this->assertIdentical($result->name, 'John', t('Name field set.'));
+    $this->assertIdentical($result->age, '0', t('Age field set to default value.'));
+    $this->assertIdentical($result->job, 'Undefined', t('Job field set to default value.'));
+    $this->assertFalse(isset($result->unknown_column), t('Unknown column was ignored.'));
+
+    // Update the newly created record.
+    $person->name = 'Peter';
+    $person->age = 27;
+    $person->job = NULL;
+    $update_result = drupal_write_record('test', $person, array('id'));
     $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.'));
 
+    // Verify that the record was updated.
+    $result = db_query("SELECT * FROM {test} WHERE id = :id", array(':id' => $person->id))->fetchObject();
+    $this->assertIdentical($result->name, 'Peter', t('Name field set.'));
+    $this->assertIdentical($result->age, '27', t('Age field set.'));
+    $this->assertIdentical($result->job, '', t('Job field set and cast to string.'));
+
+    // Try to insert NULL in columns that does not allow this.
+    $person = new stdClass();
+    $person->name = 'Ringo';
+    $person->age = NULL;
+    $person->job = NULL;
+    $insert_result = drupal_write_record('test', $person);
+    $this->assertTrue(isset($person->id), t('Primary key is set on record created with drupal_write_record().'));
+    $result = db_query("SELECT * FROM {test} WHERE id = :id", array(':id' => $person->id))->fetchObject();
+    $this->assertIdentical($result->name, 'Ringo', t('Name field set.'));
+    $this->assertIdentical($result->age, '0', t('Age field set.'));
+    $this->assertIdentical($result->job, '', t('Job field set.'));
+
+    // Insert a record - the "age" column allows NULL.
+    $person = new stdClass();
+    $person->name = 'Paul';
+    $person->age = NULL;
+    $insert_result = drupal_write_record('test_null', $person);
+    $this->assertTrue(isset($person->id), t('Primary key is set on record created with drupal_write_record().'));
+    $result = db_query("SELECT * FROM {test_null} WHERE id = :id", array(':id' => $person->id))->fetchObject();
+    $this->assertIdentical($result->name, 'Paul', t('Name field set.'));
+    $this->assertIdentical($result->age, NULL, t('Age field set.'));
+
+    // Insert a record - do not specify the value of a column that allows NULL.
+    $person = new stdClass();
+    $person->name = 'Meredith';
+    $insert_result = drupal_write_record('test_null', $person);
+    $this->assertTrue(isset($person->id), t('Primary key is set on record created with drupal_write_record().'));
+    $this->assertIdentical($person->age, 0, t('Age field set to default value.'));
+    $result = db_query("SELECT * FROM {test_null} WHERE id = :id", array(':id' => $person->id))->fetchObject();
+    $this->assertIdentical($result->name, 'Meredith', t('Name field set.'));
+    $this->assertIdentical($result->age, '0', t('Age field set to default value.'));
+
+    // Update the newly created record.
+    $person->name = 'Mary';
+    $person->age = NULL;
+    $update_result = drupal_write_record('test_null', $person, array('id'));
+    $result = db_query("SELECT * FROM {test_null} WHERE id = :id", array(':id' => $person->id))->fetchObject();
+    $this->assertIdentical($result->name, 'Mary', t('Name field set.'));
+    $this->assertIdentical($result->age, NULL, t('Age field set.'));
+
     // Run an update query where no field values are changed. The database
     // layer should return zero for number of affected rows, but
     // db_write_record() should still return SAVED_UPDATED.
-    $update_result = drupal_write_record('taxonomy_vocabulary', $vocabulary, array('vid'));
+    $update_result = drupal_write_record('test_null', $person, array('id'));
     $this->assertTrue($update_result == SAVED_UPDATED, t('Correct value returned when a valid update is run without changing any values.'));
 
     // Insert an object record for a table with a multi-field primary key.
-- 
GitLab