Steps to reproduce:

  1. Set up a Drupal DateTime field to map to a Salesforce DateTime field.
  2. Create/Update record with DateTime on Salesforce and execute Salesforce Pull cron.

Example Exception:

Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[22001]: String data, right truncated: 1406 Data too long for column 'field_date_value' at row 1: INSERT INTO {node__field_date} (entity_id, revision_id, bundle, delta, langcode, field_date_value) VALUES (:db_insert_placeholder_0,                      [error]
:db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5); Array
(
    [:db_insert_placeholder_0] => 1
    [:db_insert_placeholder_1] => 1
    [:db_insert_placeholder_2] => salesforce_ticket
    [:db_insert_placeholder_3] => 0
    [:db_insert_placeholder_4] => en
    [:db_insert_placeholder_5] => 2017-07-29T14:57:00.000+0000
)
 in Drupal\Core\Entity\Sql\SqlContentEntityStorage->saveToDedicatedTables() (line 1289 of /Users/jessetolj/Documents/Projects/drupal8-sandbox/docroot/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

Probably need similar casting for Pull as in https://www.drupal.org/node/2856995

I can try to work on a patch for this if someone can give me some guidance on the best place to implement this logic. Drupal\salesforce_mapping\Entity\MappedObject::pull() doesn't seem like the right place.

Comments

jtolj created an issue. See original summary.

aaronbauman’s picture

Thanks for the bug report.

You're right, ::pull is not a good place for this.
Per the comment there:

// @TODO: The field plugin should be in charge of setting its value on an entity, we should not assume the field plugin's logic as we're doing here.

Instead of what we're doing now, which is essentially setting the raw value directly on the drupal entity, we should delegate it to the mapping field plugin.

This would mean:

  • updating the interface Drupal\salesforce_mapping\SalesforceMappingFieldPluginInterface with a new method like "pullValue(SObject, EntityInterface, SalesforceMappingInterface)
  • providing a default implementation in Drupal\salesforce_mapping\SalesforceMappingFieldPluginBase
  • providing overrides as necessary in Drupal\salesforce_mapping\Plugin\SalesforceMappingField\* classes

Let me know if that's enough to get you started.

jtolj’s picture

Yes, makes sense. I'll try to get some time approved to work on it and make a patch. Thanks!

jtolj’s picture

Here's a first pass at this.

I'm not clear on whether RelatedProperties and RelatedIDs require an override for the pullValue() method (pushValue() is not overridden). I don't have data handy to test those with but can get something set up if it looks like the default implementation won't work.

I've tested property mapping for boolean, date, datetime, picklist, multipicklist, and SFID with this patch applied and all are pulling correctly for me.

Picklist and multipicklist obviously depend on matching option keys being set up on the Drupal end.

Thanks!

aaronbauman’s picture

Status: Active » Needs review

Taking a look now.
Let's see what testbot says too.

I'm not clear on whether RelatedProperties and RelatedIDs require an override for the pullValue() method (pushValue() is not overridden). I don't have data handy to test those with but can get something set up if it looks like the default implementation won't work.

These plugins probably need overrides in both directions.
I've been mostly working with Properties, so haven't given them the attention they deserve.

aaronbauman’s picture

Status: Needs review » Needs work

Looks like you're headed in the right direction so far - thanks for the patch!

+    if (!$this->pull() || empty($this->config('salesforce_field'))) {
+      return $value;
+    }

Maybe we should throw an exception here, rather than returning a value, so that the caller knows for sure that this value doesn't make sense.

+    try {
+      $field_definition = $describe->getField($this->config('salesforce_field'));
+    }
+    catch (\Exception $e) {
+      $this->eventDispatcher->dispatch(SalesforceEvents::WARNING, new SalesforceWarningEvent($e, 'Field definition not found for %describe.%field', ['%describe' => $describe->getName(), '%field' => $this->config('salesforce_field')]));
+      // If getField throws, however, just return the raw value.
+      return $value;
+    }

Same here. Let's allow this exception to bubble up so that the caller doesn't think our transformation was successful.

+    if (is_string($value) && $field_definition['length'] > 0 && strlen($value) > $field_definition['length']) {
+      $value = substr($value, 0, $field_definition['length']);
+    }

Let's put this into a default case.
substr() doesn't make sense for the other cases of the switch statements.

In the meantime, I'll fix our broken test coverage...

jtolj’s picture

Great, I'll make those changes tomorrow. Thanks for taking a look so quickly!

jtolj’s picture

Updated patch attached. Thanks!

aaronbauman’s picture

Status: Needs work » Needs review
tauno’s picture

It looks like SalesforceMappingFieldPluginBase::pullValue() is manipulating the values based on what the Salesforce field definitions are rather than what the Drupal field definitions are. Since the data is intended to end up in Drupal fields shouldn't we be looking at that? The most obvious example of an issue is the default case where the value from Salesforce is trimmed to the max length the Salesforce field defines - in other words nothing would happen.

Agreed that related entities and their properties will need an override to map the SF Id to a Drupal Id.

tauno’s picture

Status: Needs review » Needs work
tauno’s picture

Updated patch to include RelatedIds override.

jtolj’s picture

@tuano

Thanks for looking it over and implementing the RelatedIds override.

I definitely agree that the max length should be coming from the Drupal field. I'll make that change.

For the other field types, my thought was the goal should be transforming the data type coming from Salesforce into what Drupal might expect for a similar field. It would be up to the user to map them rationally.

For example, the multipicklist Salesforce type - we need knowledge here of how it's coming in from Salesforce (semicolon delimited) to restructure it into something that Drupal would expect.

Let me know your thoughts and I can make adjustments if needed.

Thanks!

aaronbauman’s picture

+    try {
+      $field_definition = $describe->getField($this->config('salesforce_field'));
+    }
+    catch (\Exception $e) {
+      $this->eventDispatcher->dispatch(SalesforceEvents::WARNING, new SalesforceWarningEvent($e, 'Field definition not found for %describe.%field', ['%describe' => $describe->getName(), '%field' => $this->config('salesforce_field')]));
+
+      throw new SalesforceException(
+        sprintf('Field definition not found for %s.%s', $describe->getName(), $this->config('salesforce_field'))
+      );
+    }

I think we don't need to handle this exception at all.
Just allow it to bubble up, and allow the caller to dispatch an event as applicable.

I'll just echo Tauno's point again, and add some specific detail.
We definitely need some manipulation based on both Drupal and Salesforce fields, e.g. for multipicklist and booleans.

+      case 'date':
+        $tmp = $value;
+        if (!is_int($tmp)) {
+          $tmp = strtotime($tmp);
+        }
+        if (!empty($tmp)) {
+          $value = $this->dateFormatter->format($tmp, 'custom', DATETIME_DATE_STORAGE_FORMAT, 'UTC');
+        }
+        break;
+
+      case 'datetime':
+        $tmp = $value;
+        if (!is_int($tmp)) {
+          $tmp = strtotime($tmp);
+        }
+        if (!empty($tmp)) {
+          $value = $this->dateFormatter->format($tmp, 'custom', DATETIME_DATETIME_STORAGE_FORMAT, 'UTC');
+        }
+        break;

For dates, let's not bother changing the format at this point.
If, for example, i have a SF date field mapped to a Drupal textfield, I don't need to do any manipulation.
If I'm mapping a SF datetime to a Drupal date, I need to strip off the "time" component.

+      case 'double':
+        $value = (double) $value;
+        break;
+
+      case 'integer':
+        $value = (int) $value;
+        break;
+
+      case 'multipicklist':
+        if (!is_array($value)) {
+          $value = explode(';', $value);
+          $value = array_map('trim', $value);
+        }
+        break;
+
+      case 'id':
+      case 'reference':
+        if (empty($value)) {
+          break;
+        }
+        // If value is an SFID, cast to string.
+        if ($value instanceof SFID) {
+          $value = (string) $value;
+        }
+        // Otherwise, send it through SFID constructor & cast to validate.
+        else {
+          $value = (string) (new SFID($value));
+        }
+        break;

These are fine I think, but we also need to ensure they make sense for Drupal.
For example, if a SF double is mapped to a Drupal integer, it needs to be re-cast.
Or, if multi-valued SF multi-picklist is mapped to a single-valued Drupal field, we need to reconcile that as well.

Thanks again for your work on this.

jtolj’s picture

I think we don't need to handle this exception at all.
Just allow it to bubble up, and allow the caller to dispatch an event as applicable.

Will do.

For dates, let's not bother changing the format at this point.
If, for example, i have a SF date field mapped to a Drupal textfield, I don't need to do any manipulation.
If I'm mapping a SF datetime to a Drupal date, I need to strip off the "time" component.

Okay, so essentially we would need conditional handling inside of each Salesforce type depending on the Drupal field type that it is mapped to. That makes sense in a lot of cases (like the above).

But for things like this:

For example, if a SF double is mapped to a Drupal integer, it needs to be re-cast.
Or, if multi-valued SF multi-picklist is mapped to a single-valued Drupal field, we need to reconcile that as well.

My concern is that bi-directional sync would cause data loss. For example, let's say I have (double) 1.25 that I have mapped to an integer field in Drupal, and we cast it to (int) 1. Now when I re-save the node, and things are synced back to Salesforce it is re-cast to (double) 1.0 and I've lost data. Similarly, if I have a multipicklist mapped to a select that only allows one value, we could lose data going back to Salesforce.

Are we concerned about this at all, or is it just on the user to map these in a way that makes sense?

Thanks!

tauno’s picture

StatusFileSize
new11.54 KB

Missed a use statement in the patch in #12.

aaronbauman’s picture

Okay, so essentially we would need conditional handling inside of each Salesforce type depending on the Drupal field type that it is mapped to. That makes sense in a lot of cases (like the above).

Yeah... this could get messy though, so let's not go too crazy.
IMO if there's anything that's not obvious to map, throw an exception and let the user sort it out.

My concern is that bi-directional sync would cause data loss.

Yeah, there's a lot of ways that folks could set up mappings that would break things badly.
There's only so much we can take responsibility for defending against though, and I'd say this example is out of scope.

jtolj’s picture

Updated with changes requested (this includes tuano's patch in #16).

  1. Switched datetime to truncate incoming value rather than run through date formatter, but only when mapped to a datetime field in Drupal.
  2. Used Drupal field definition to determine if an incoming string need to be truncated.

It looks to me from testing the Drupal handles casting the other cases on it's own. So if I map a float field in salesforce to an integer field in Drupal, it is cast correctly without any handling in this code (1.22 stores as 1 in Drupal). Similarly with multiselect, it will take the first x items of the array where x is the allowed number of values configured in field settings.

Thanks!

aaronbauman’s picture

Status: Needs work » Needs review
aaronbauman’s picture

A couple changes, biggest of which is to swap the hard-coded "value" with getMainPropertyName()
Also a missing "use" statement (should have been caught by tests - obviously we need more coverage).

Otherwise this is the same.

This is going to change when we update mapping field plugins to accommodate properties: #2899460: Handling of field properties
But I think it's probably fine to get this in now.

  • aaronbauman committed 8dc45a9 on 8.x-3.x
    Issue #2897281 by jtolj, tauno, aaronbauman: "String data, right...
aaronbauman’s picture

Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.