5

I inherited maintenance for some Apex code that performs a type of assignment I have not seen before.

Given these two maps:

Map<id, feedItem> caseIdToFeedItemMap = new Map<id, feedItem>();
...
// Code that adds member to caseIdToFeedItemMap
...
Map<String, Object> params = new Map<String, Object>();

I can't seem to find any Salesforce documentation that this assignment is valid:

params.put('cCaseId', caseIdToFeedItemMap.values().ParentId);

The Map method caseIdToFeedItemMap.values() should produce a List. ListVariable.Property does not seem like a valid expression. You could iterate the List and reference that property. You could use that property on a specific List item ListVariable[0].Property.

It may be perfectly valid, but I have not seen this before and I can't seem to find it in the Apex documentation. The assigned variable is used as a parameter for invoking a flow.

Does this seem like a valid assignment? Is it documented or implied in documentation?

Edit: I neglected to mention that the subject assignment is used to prepare a parameter to invoke a flow in a bulkified FeedItem trigger handler. If it only works when there is one record, our trigger to flow process is broken. This probably says something about the quality of our unit testing.

11
  • I duplicated this (V58) - my IDE (Illuminated Cloud) says ParentId is an invalid symbol but Apex compiler accepts it; at runtime, if list size is one, returns the 0th element.ParentId; if list size is > 1, returns runtime error System.QueryException: List has more than 1 row for assignment to SObject
    – cropredy
    Sep 8, 2023 at 21:37
  • Looks like @cropredy has it. It's a oh-so-convenient compiler optimization with dereferencing a property on 1-element lists that was done for inlined SOQL. Unfortunately it appears to work in non-SOQL expressions.
    – identigral
    Sep 8, 2023 at 23:19
  • I'll let the PM know
    – cropredy
    Sep 9, 2023 at 0:35
  • This isn't a bug...
    – sfdcfox
    Sep 9, 2023 at 2:05
  • 1
    @sfdcfox There's no Apex language specification so the expected behavior is unknown. Having production code rely on an undocumented side effect of squishing a list to a scalar is certainly criminal.
    – identigral
    Sep 9, 2023 at 4:00

2 Answers 2

1

I duplicated this (V58)

  • my IDE (Illuminated Cloud) says ParentId is an invalid symbol but Apex compiler accepts it;
  • at runtime, if list size is one, returns the 0th element.ParentId;
  • if list size is > 1, returns runtime error System.QueryException: List has more than 1 row for assignment to SObject

In the code example you showed, it would only work if you knew that values() always returned exactly one row, never two or more. This is not great practice as one should be thinking about Map class method values() as always returning a list of size 0,1,2, ... These elements could be primitives, apextypes, or sobjects depending on the Map's declaration.

  • I'd be more explicit about it by doing:

    // only care about 1st value
    params.put('cCaseId', caseIdToFeedItemMap.values()[0].ParentId); 
    
2
  • Thank you for verifying that an exception is thrown if there is more than one record. In my opinion, the discussion can end there. I use VS Code and the replay debugger. I would not be able to reproduce this with the mess we have in our unit test. In our case we need to call the flow with a scalar, so I'll need to iterate the List<FeedItem> and invoke the flow with each value. It's fun to find something that can be improved. Thanks again! Sep 11, 2023 at 1:06
  • N.B. Issue logged in Illuminated Cloud
    – cropredy
    Sep 11, 2023 at 20:52
3

What you've run into is a feature that's documented as Using SOQL Queries That Return One Record. Basically, there are some cases where you can assign a list that has a single item in it to a scalar value. Despite what the documentation says, you can use this in a variety of ways that are not specifically called out in the documentation, notably that this is a feature baked into the language itself, and not specifically to SOQL.

To understand what's going behind the scenes, there is an object that looks like a QueryResult. Apex uses this object transparently in order to facilitate the ability to behave the way it is documented. You'll see this in times when you JSON.serialize a query directly, and you'll find it has some specific behaviors. This object tends to not be null, and has a language-specific feature that allows you to access the first element if, and only if, exactly one item is in the list. It is also responsible for facilitating query cursors when there are too many results to fit in memory all at once.

As a simple example, let us write the following code:

Contact c = (Contact)JSON.deserialize(
    '{"Cases":{"done": false, "totalSize": 0, "records": []}}',
    Contact.class
);
Case[] cases = c.Cases;

When this code runs, we get the exception:

System.QueryException: Aggregate query has too many rows for direct assignment, use FOR loop

This happened because the underlying QueryResult object detected that the list was incomplete ("done": false).

Similarly, if we have exactly one entry, we can use the "Using SOQL Queries That Return One Record" behavior.

Contact c = (Contact)JSON.deserialize(
    '{"Cases":{"done": true, "totalSize": 1, "records": [{"Subject":"Need Help"}]}}',
    Contact.class
);
Case theCase = c.Cases;
System.debug(theCase.Subject); // Need Help

This code does not crash, as there is exactly one record to return. As long as the internal QueryResult meets the conditions that (1) done is true, (2) totalSize is 1, and (3) records contains exactly one record, you can use this behavior.

Interestingly, Map.values() appears to return a QueryResult object internally. This is inconsistent with the documentation for Map. However, I think this was documented this way intentionally, since we aren't necessarily meant to "know" what's going on in the backend. However, we know that this must be true, because the following code also throws the QueryException from above:

Map<Id, Case> cases = new Map<Id, Case>();
Case theCaseValues = cases.values();

If Map.values() did not return this QueryResult, then the language would not work according to its documented behavior. After all, we can't assign a normal list this way:

Case[] cases = new Case[0];
// Compiler error: Illegal assignment from List<Case> to Case
Case theCase = cases;

It is a shame that we only have the one page in the documentation that suggests this is possible. While it is not strictly "undocumented," I would say that it is poorly documented. Apex is working in the manner that it was documented in. Now that you know this feature exists, and has literally existed since at least the first public release of Apex, you can understand why it works.

I'd like to add that the Apex Language Server does not throw an error in VS Code when you write code that looks like this. The bug here is that any IDE that does not use the official Apex Language Server may be inconsistent with the actual implementation of Apex. Illuminated Cloud simply has a detail wrong about the underlying implementation, and I wouldn't blame them, as this is a rather niche construct.

Also, unless it is very explicitly proven to be a bug, which I don't think this is, this is more of a documentation omission. Salesforce R&D typically maintain that the behavior produced by the server is the correct behavior, and that the documentation is incorrect, instead.

That said, if you're going to use the behavior intentionally, you should leave a comment about how there's only ever one record returned, etc. You usually want to be more specific by selecting the first index, if that is your intent (someValues[0]).

4
  • I knew you'd have a better answer than me <g>. I imagine that if the map's values were apextypes, it would not return a QueryResult. Ultimately, using the [0] index would be best practice as you note on the last line.
    – cropredy
    Sep 10, 2023 at 18:22
  • @cropredy This was actually addressed in my other now deleted answer. If you try it with a non-sObject type, it actually does compile, but throws an Internal Server Error at runtime.
    – sfdcfox
    Sep 10, 2023 at 18:28
  • @sfdcfox, Thank you for the massive explanation. Unfortunately (for me) the "develop with clicks not code" stuff is significantly harder to work with than Apex. On first look, it appears that the flow expects a scalar. So I need to iterate the Map.Values() (List<FeedItem>) and invoke the flow N times with a scalar parameter value. It's apparently been failing any time the trigger calls the handler with more than one FeedItem. Sep 11, 2023 at 0:59
  • Interesting discussion. One of my users brought it to my attention, and I'm going through a bit of an internal debate on what to do about it in IC2, if anything. As stated in the linked bug report, I'll probably go ahead and fix it, but I'll probably also add a warning-level code inspection about its usage, likely with quick fixes to make that usage more defensive. Thanks for providing the detailed analysis, @sfdcfox! Sep 11, 2023 at 18:56

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .