=== modified file 'uc_attribute/uc_attribute.module'
--- uc_attribute/uc_attribute.module	2009-06-10 20:28:03 +0000
+++ uc_attribute/uc_attribute.module	2009-06-15 05:34:04 +0000
@@ -456,86 +456,520 @@ function uc_attribute_cart_item_descript
  ******************************************************************************/
 
 /**
- * Load an attribute from the database.
- *
- * @param $attr_id
- *   The id of the attribute.
- * @param $nid
- *   Node id. If given, the attribute will have the options that have been
- *   assigned to that node for the attribute.
+ * Load attribute objects from the database.
+ * 
+ * @todo If we feel it necessary, we could optimize this, by inverting the
+ *  logic; that is, we could make uc_attribute load call this function and allow
+ *  this function to minimize the number of queries necessary. -cha0s
+ *  
+ * @param $aids
+ *   Attribute IDs to load.
+ * @param $type
+ *   The type of attribute. 'product', or 'class'. Any other type will fetch
+ *   a base attribute
+ * @param $id
+ *   The ID of the product/class this attribute belongs to.
+ * @return (array)
+ *   The array of loaded attributes.
  */
-function uc_attribute_load($attr_id, $nid = NULL, $type = '') {
-  if ($nid) {
-    switch ($type) {
-      case 'product':
-        $attribute = db_fetch_object(db_query("SELECT a.aid, a.name, a.label AS default_label, a.ordering AS default_ordering, a.required AS default_required, a.display AS default_display, a.description, pa.label, pa.default_option, pa.required, pa.ordering, pa.display FROM {uc_attributes} AS a LEFT JOIN {uc_product_attributes} AS pa ON a.aid = pa.aid AND pa.nid = %d WHERE a.aid = %d", $nid, $attr_id));
-        $result = db_query("SELECT po.nid, po.oid, po.cost, po.price, po.weight, po.ordering, ao.name, ao.aid FROM {uc_product_options} AS po LEFT JOIN {uc_attribute_options} AS ao ON po.oid = ao.oid AND nid = %d WHERE aid = %d ORDER BY po.ordering, ao.name", $nid, $attr_id);
-        break;
-      case 'class':
-        $attribute = db_fetch_object(db_query("SELECT a.aid, a.name, a.label AS default_label, a.ordering AS default_ordering, a.required AS default_required, a.display AS default_display, a.description, ca.default_option, ca.label, ca.required, ca.ordering, ca.display FROM {uc_attributes} AS a LEFT JOIN {uc_class_attributes} AS ca ON a.aid = ca.aid AND ca.pcid = '%s' WHERE a.aid = %d", $nid, $attr_id));
-        $result = db_query("SELECT co.pcid, co.oid, co.cost, co.price, co.weight, co.ordering, ao.name, ao.aid FROM {uc_class_attribute_options} AS co LEFT JOIN {uc_attribute_options} AS ao ON co.oid = ao.oid AND co.pcid = '%s' WHERE ao.aid = %d ORDER BY co.ordering, ao.name", $nid, $attr_id);
-        break;
-      default:
-        $attribute = db_fetch_object(db_query("SELECT * FROM {uc_attributes} WHERE aid = %d", $attr_id));
-        $result = db_query("SELECT * FROM {uc_attribute_options} WHERE aid = %d ORDER BY ordering, name, label", $attr_id);
-        break;
-    }
-    if (isset($attribute->default_ordering) && is_null($attribute->ordering)) {
-      $attribute->ordering = $attribute->default_ordering;
-    }
-    if (isset($attribute->default_required) && is_null($attribute->required)) {
-      $attribute->required = $attribute->default_required;
-    }
-    if (isset($attribute->default_display) && is_null($attribute->display)) {
-      $attribute->display = $attribute->default_display;
-    }
-    if (isset($attribute->default_label) && is_null($attribute->label)) {
-      $attribute->label = $attribute->default_label;
-    }
-    if (empty($attribute->label)) {
-      $attribute->label = $attribute->name;
-    }
+function uc_attribute_load_multiple($aids = array(), $type = '', $id = NULL) {
+  $sql = uc_attribute_type_info($type);
+  $conditions = array();
+  
+  // Filter by the attribute IDs requested.
+  if (!empty($aids)) {
+    // Sanity check - filter out non-numeric attribute IDs.
+    $conditions[] = "ua.aid IN (". implode(", ", array_filter($aids, 'is_numeric')) .")";
+  }
+  
+  // Product/class attributes.
+  if (!empty($type)) {
+    $conditions[] = "uca.{$sql['id']} = {$sql['placeholder']}";
+    $conditions = implode(" AND", $conditions);
+    // Seems like a big query to get attribute IDs, but it's all about the sort.
+    // (I'm not sure if the default ordering is propagating down correctly here.
+    // It appears that product/class attributes with no ordering won't let the 
+    // attribute's propagate down, as it does when loading. -cha0s) 
+    $result = db_query("
+      SELECT    uca.aid 
+      FROM      {$sql['attr_table']} AS uca 
+      LEFT JOIN {uc_attributes} AS ua ON uca.aid = ua.aid 
+      WHERE     $conditions
+      ORDER BY  uca.ordering, ua.name", $id);
   }
+  
+  // Base attributes.
   else {
-    $attribute = db_fetch_object(db_query("SELECT * FROM {uc_attributes} WHERE aid = %d", $attr_id));
-    $result = db_query("SELECT * FROM {uc_attribute_options} WHERE aid = %d ORDER BY ordering, name", $attr_id);
+    // Padding just to make sure that everything's fine if we don't get an aid
+    // condition. Keeps it elegant.
+    $conditions[] = "1";
+    $conditions = implode(" AND ", $conditions);
+    
+    $result = db_query("SELECT aid FROM {uc_attributes} ua WHERE $conditions ORDER BY ordering, name"); 
+  }
+  
+  // Load the attributes.
+  $attributes = array();
+  while ($aid = db_result($result)) {
+    $attributes[$aid] = uc_attribute_load($aid, $type, $id);
+  }
+
+  return $attributes;
+}
+
+/**
+ * Load an attribute from the database.
+ *
+ * @param $aid
+ *   The ID of the attribute.
+ * @param $type
+ *   The type of attribute. 'product', or 'class'. Any other type will fetch
+ *   a base attribute
+ * @param $id
+ *   The ID of the product/class this attribute belongs to.
+ * @return
+ *   The attribute object, or FALSE if it doesn't exist.
+ */
+function uc_attribute_load($aid, $type = '', $id = NULL) {
+  $sql = uc_attribute_type_info($type);
+  
+  switch ($type) {
+    case 'product':
+    case 'class':
+      
+      // Read attribute data.
+      $attribute = db_fetch_object(db_query("
+        SELECT    a.aid, a.name, a.label AS default_label, a.ordering AS default_ordering, 
+                  a.required AS default_required, a.display AS default_display, 
+                  a.description, pa.label, pa.default_option, pa.required, pa.ordering, 
+                  pa.display, pa.{$sql['id']}
+        FROM      {uc_attributes} AS a 
+        LEFT JOIN {$sql['attr_table']} AS pa ON a.aid = pa.aid AND 
+                  pa.{$sql['id']} = {$sql['placeholder']}
+        WHERE a.aid = %d", $id, $aid));
+      
+      // Don't try to build it further if it failed already.
+      if (!$attribute) return FALSE;
+      
+      // Set any missing defaults.
+      foreach (array('ordering', 'required', 'display', 'label') as $field) {
+        if (isset($attribute->{"default_$field"}) && is_null($attribute->$field)) {
+          $attribute->$field = $attribute->{"default_$field"};
+        }
+      }
+      if (empty($attribute->label)) {
+        $attribute->label = $attribute->name;
+      }
+      
+      // Read option data.
+      $result = db_query("
+        SELECT    po.{$sql['id']}, po.oid, po.cost, po.price, po.weight, po.ordering, ao.name, 
+                  ao.aid 
+        FROM      {$sql['opt_table']} AS po 
+        LEFT JOIN {uc_attribute_options} AS ao ON po.oid = ao.oid AND 
+                  po.{$sql['id']} = {$sql['placeholder']} 
+        WHERE     aid = %d ORDER BY po.ordering, ao.name", $id, $aid);
+            
+    break;
+    
+    default:
+      
+      // Read attribute and option data.
+      $attribute = db_fetch_object(db_query("SELECT * FROM {uc_attributes} WHERE aid = %d", $aid));
+      $result = db_query("SELECT * FROM {uc_attribute_options} WHERE aid = %d ORDER BY ordering, name", $aid);
+      
+      // Don't try to build it further if it failed already.
+      if (!$attribute) return FALSE;
+      
+    break;
   }
+
+  // Got an attribute?
   if ($attribute) {
+    // Get its options, too.
     $attribute->options = array();
     while ($option = db_fetch_object($result)) {
       $attribute->options[$option->oid] = $option;
     }
   }
+    
   return $attribute;
 }
 
-// Loads the option identified by $oid.
+/**
+ * Fetch an array of attribute objects from the database who belong to a product.
+ * 
+ * @param $nid
+ *   Product whose attributes to load.
+ * @return (array)
+ *   The array of attribute objects.
+ */
+function uc_attribute_load_product_attributes($nid) {
+  return uc_attribute_load_multiple(array(), 'product', $nid);
+}
+
+/**
+ * Save an attribute object to the database.
+ * 
+ * @param $attribute
+ *   The attribute object to save.
+ * @return (integer)
+ *   Return the result from drupal_write_record().
+ */
+function uc_attribute_save(&$attribute) {
+  // Insert or update?
+  $key = empty($attribute->aid) ? NULL : 'aid';
+  return drupal_write_record('uc_attributes', $attribute, $key);
+}
+
+/**
+ * Delete an attribute from the database.
+ * 
+ * @param $aid
+ *   Attribute ID to delete.
+ * @return (integer)
+ *   Return the Drupal SAVED_DELETED flag.
+ */
+function uc_attribute_delete($aid) {
+  // Delete the class attributes and their options.
+  uc_attribute_subject_delete($aid, 'class');
+  
+  // Delete the product attributes and their options.
+  uc_attribute_subject_delete($aid, 'product');
+  
+  // Delete base attributes and their options.
+  db_query("DELETE FROM {uc_attribute_options} WHERE aid = %d", $aid);
+  db_query("DELETE FROM {uc_attributes} WHERE aid = %d", $aid);
+  
+  return SAVED_DELETED;
+}
+
+/**
+ * Load an attribute option from the database.
+ * 
+ * @param $oid
+ *   Option ID to load.
+ * @return (object)
+ *   The attribute option object.
+ */
 function uc_attribute_option_load($oid) {
   return db_fetch_object(db_query("SELECT * FROM {uc_attribute_options} WHERE oid = %d", $oid));
 }
 
-// Loads all attributes associated with a product node.
-function uc_product_get_attributes($nid) {
-  $attributes = array();
+/**
+ * Save an attribute object to the database.
+ * 
+ * @param $option
+ *   The attribute option object to save.
+ * @return (integer)
+ *   Return the result from drupal_write_record().
+ */
+function uc_attribute_option_save(&$option) {
+  // Insert or update?
+  $key = empty($option->oid) ? NULL : 'oid';
+  return drupal_write_record('uc_attribute_options', $option, $key);
+}
+
+/**
+ * Delete an attribute option from the database.
+ * 
+ * @param $oid
+ *   Option ID to delete.
+ * @return (integer)
+ *   Return the Drupal SAVED_DELETED flag.
+ */
+function uc_attribute_option_delete($oid) {
+  // Delete the class attribute options.
+  uc_attribute_subject_option_delete($oid, 'class');
+  
+  // Delete the product attribute options. (and the adjustments!)
+  uc_attribute_subject_option_delete($oid, 'product');
+  
+  // Delete base attributes and their options.
+  db_query("DELETE FROM {uc_attribute_options} WHERE oid = %d", $oid);
+  
+  return SAVED_DELETED;
+}
+
+/**
+ * Save a product/class attribute.
+ *
+ * @param &$attribute
+ *   The product/class attribute.
+ * @param $type
+ *   Is this a product or a class?
+ * @param $id
+ *   The product/class ID.
+ * @param $save_options
+ *   Save the product/class attribute's options, too?
+ * @return (integer)
+ *   Return the result from drupal_write_record().
+ */
+function uc_attribute_subject_save(&$attribute, $type, $id, $save_options = FALSE) {
+  $sql = uc_attribute_type_info($type);
 
-  $result = db_query("SELECT upa.aid FROM {uc_product_attributes} AS upa LEFT JOIN {uc_attributes} AS ua ON upa.aid = ua.aid WHERE upa.nid = %d ORDER BY upa.ordering, ua.name", $nid);
-  while ($attribute = db_fetch_object($result)) {
-    $attributes[$attribute->aid] = uc_attribute_load($attribute->aid, $nid, 'product');
+  // Insert or update?
+  $key = uc_attribute_subject_exists($attribute->aid, $type, $id) ? array('aid', $sql['id']) : NULL;
+
+  // First, save the options. First because if this is an insert, we'll set
+  // a default option for the product/class attribute.
+  if ($save_options && is_array($attribute->options)) {
+    foreach ($attribute->options as $option) {
+      // Sanity check!
+      $option = (object) $option;
+      uc_attribute_subject_option_save($option, $type, $id);
+    }
+
+    // Is this an insert? If so, we'll set the default option.
+    if (empty($key)) {
+      $default_option = 0;
+      // Make the first option (if any) the default.
+      if (is_array($attribute->options)) {
+        $option = (object) reset($attribute->options);
+        $default_option = $option->oid;
+      }
+      $attribute->default_option = $default_option;
+    }
   }
 
-  return $attributes;
+  // Merge in the product/class attribute's ID and save.
+  $attribute->{$type == 'product' ? 'nid' : 'pcid'} = $id;
+  $result = drupal_write_record(trim($sql['attr_table'], '{}'), $attribute, $key);
+
+  return $result;
 }
 
-// Loads all attributes associated with a product class.
-function uc_class_get_attributes($pcid) {
-  $attributes = array();
+/**
+ * Delete all the options associated with this product/class attribute, and
+ * then the attribute itself.
+ *
+ * @param $aid
+ *   The base attribute ID.
+ * @param $type
+ *   Is this a product or a class?
+ * @param $id
+ *   The product/class ID.
+ * @return (integer)
+ *   Return the Drupal SAVED_DELETED flag.
+ */
+function uc_attribute_subject_delete($aid, $type, $id = NULL) {
+  $sql = uc_attribute_type_info($type);
 
-  $result = db_query("SELECT uca.aid FROM {uc_class_attributes} AS uca LEFT JOIN {uc_attributes} AS ua ON uca.aid = ua.aid WHERE uca.pcid = '%s' ORDER BY uca.ordering, ua.name", $pcid);
-  while ($attribute = db_fetch_object($result)) {
-    $attributes[$attribute->aid] = uc_attribute_load($attribute->aid, $pcid, 'class');
+  // Base conditions, and an ID check if necessary.
+  $conditions[] = "aid = %d";
+  if ($id) {
+    $conditions[] = "{$sql['id']} = {$sql['placeholder']}";
+  }
+  $conditions = implode(" AND ", $conditions);
+  
+  $result = db_query("SELECT a.oid FROM {uc_attribute_options} AS a JOIN {$sql['opt_table']} AS subject ON a.oid = subject.oid WHERE $conditions", $aid, $id);
+  while ($oid = db_result($result)) {
+    // Don't delete the adjustments one at a time. We'll do it in bulk soon for
+    // efficiency.
+    uc_attribute_subject_option_delete($oid, $type, $id, FALSE);
+  }
+  db_query("DELETE FROM {$sql['attr_table']} WHERE $conditions", $aid, $id);
+
+  // If this is a product attribute, wipe any associated adjustments.
+  if ($type == 'product') {
+    uc_attribute_adjustments_delete(array(
+      'aid' => $aid,
+      'nid' => $id,
+    ));
   }
+  
+  return SAVED_DELETED;
+}
 
-  return $attributes;
+/**
+ * Load a product/class attribute option.
+ *
+ * @param $oid
+ *   The product/class attribute option ID.
+ * @param $type
+ *   Is this a product or a class?
+ * @param $id
+ *   The product/class ID.
+ * @return (object)
+ *   Return the product/class attribute option.
+ */
+function uc_attribute_subject_option_load($oid, $type, $id) {
+  $sql = uc_attribute_type_info($type);
+
+  $result = db_query("
+    SELECT    po.{$sql['id']}, po.oid, po.cost, po.price, po.weight, po.ordering, ao.name, 
+              ao.aid 
+    FROM      {$sql['opt_table']} AS po 
+    LEFT JOIN {uc_attribute_options} AS ao ON po.oid = ao.oid AND 
+              po.{$sql['id']} = {$sql['placeholder']} 
+    WHERE     po.oid = %d ORDER BY po.ordering, ao.name", $id, $oid);
+  
+  return db_fetch_object($result);
+}
+
+/**
+ * Save a product/class attribute option.
+ *
+ * @param &$option
+ *   The product/class attribute option.
+ * @param $type
+ *   Is this a product or a class?
+ * @param $id
+ *   The product/class ID.
+ * @return (integer)
+ *   Return the result from drupal_write_record().
+ */
+function uc_attribute_subject_option_save(&$option, $type, $id) {
+  $sql = uc_attribute_type_info($type);
+
+  // Insert or update?
+  $key = uc_attribute_subject_option_exists($option->oid, $type, $id) ? array('oid', $sql['id']) : NULL;
+
+  // Merge in the product/class attribute option's ID, and save.
+  $option->{$type == 'product' ? 'nid' : 'pcid'} = $id;
+  $result = drupal_write_record(trim($sql['opt_table'], '{}'), $option, $key);
+
+  return $result;
+}
+
+/**
+ * Delete a product/class attribute option.
+ *
+ * @param $oid
+ *   The base attribute's option ID.
+ * @param $type
+ *   Is this a product or a class?
+ * @param $id
+ *   The product/class ID.
+ * @return (integer)
+ *   Return the Drupal SAVED_DELETED flag.
+ */
+function uc_attribute_subject_option_delete($oid, $type, $id = NULL, $adjustments = TRUE) {
+  $sql = uc_attribute_type_info($type);
+
+  // Base conditions, and an ID check if necessary.
+  $conditions[] = "oid = %d";
+  if ($id) {
+    $conditions[] = "{$sql['id']} = {$sql['placeholder']}";
+  }
+  $conditions = implode(" AND ", $conditions);
+  
+  // Delete the option.
+  db_query("DELETE FROM {$sql['opt_table']} WHERE $conditions", $oid, $id);
+
+  // If this is a product, clean up the associated adjustments.
+  if ($adjustments && $type == 'product') {
+    uc_attribute_adjustments_delete(array(
+      'aid' => uc_attribute_option_load($oid)->aid,
+      'oid' => $oid,
+      'nid' => $id,
+    ));
+  }
+  
+  return SAVED_DELETED;
+}
+
+/**
+ * @param $fields
+ *   Fields used to build a condition to delete adjustments against. Fields
+ *   currently handled are 'aid', 'oid', and 'nid'.
+ * @return (integer)
+ *   Return the Drupal SAVED_DELETED flag.
+ */
+function uc_attribute_adjustments_delete($fields) {
+  
+  // Build the serialized string to match against adjustments.
+  $match = '';
+  if (!empty($fields['aid'])) {
+    $match .= serialize((integer) $fields['aid']);
+  }
+  if (!empty($fields['oid'])) {
+    $match .= serialize((string) $fields['oid']);
+  }
+  
+  // Assemble the conditions and args for the SQL.
+  $args = $conditions = array();
+  
+  // If we have to match aid or oid...
+  if ($match) {
+    $conditions[] = "combination LIKE '%%%s%%'";
+    $args[] = $match;
+  }
+  
+  // If we've got a node ID to match.
+  if (!empty($fields['nid'])) {
+    $conditions[] = "nid = %d";
+    $args[] = $fields['nid'];
+  }
+  $conditions = implode(" AND ", $conditions);
+  
+  // Delete what's necessary,
+  if ($conditions) {
+    db_query("DELETE FROM {uc_product_adjustments} WHERE $conditions", $args);
+  }
+  
+  return SAVED_DELETED;
+}
+
+/**
+ * Check if a product/class attribute exists.
+ *
+ * @param $aid
+ *   The base attribute ID.
+ * @param $id
+ *   The product/class attribute's ID.
+ * @param $type
+ *   Is this a product or a class?
+ * @return (bool)
+ */
+function uc_attribute_subject_exists($aid, $type, $id) {
+  $sql = uc_attribute_type_info($type);
+  return FALSE !== db_result(db_query("SELECT aid FROM {$sql['attr_table']} WHERE aid = %d AND {$sql['id']} = {$sql['placeholder']}", $aid, $id));
+}
+
+/**
+ * Check if a product/class attribute option exists.
+ *
+ * @param $oid
+ *   The base attribute option ID.
+ * @param $id
+ *   The product/class attribute option's ID.
+ * @param $type
+ *   Is this a product or a class?
+ * @return (bool)
+ */
+function uc_attribute_subject_option_exists($oid, $type, $id) {
+  $sql = uc_attribute_type_info($type);
+  return FALSE !== db_result(db_query("SELECT oid FROM {$sql['opt_table']} WHERE oid = %d AND {$sql['id']} = {$sql['placeholder']}", $oid, $id));
+}
+
+/**
+ * Return a list of db helpers to abstract the queries between products/classes.
+ * @param $type
+ *   Is this a product or a class?
+ * @return (array)
+ *   Information helpful for creating SQL queries dealing with attributes.
+ */
+function uc_attribute_type_info($type) {
+  switch ($type) {
+    case 'product':
+      return array(
+        'attr_table' => '{uc_product_attributes}',
+        'opt_table' => '{uc_product_options}',
+        'id' => 'nid',
+        'placeholder' => '%d',
+      );
+    break;
+
+    case 'class':
+      return array(
+        'attr_table' => '{uc_class_attributes}',
+        'opt_table' => '{uc_class_attribute_options}',
+        'id' => 'pcid',
+        'placeholder' => "'%s'",
+      );
+    break;
+  }
 }
 
 /**
