I was answering a question and this little inconsistency popped up. Just wondering if anyone has come across it before, or if anyone knows why this happens.
A variable that is declared within some form of scoped element (
if
,for
loop, etc.) shouldn’t be available outside that scope.i.e.
{ // accountNames variable is not available outside this scope Set<String> accountNames = new Set<String> {'Test Account'}; } system.debug(accountNames);
This debug statement fails with the message:
Variable does not exist: accountNames
However, if this is part of a query string being generated, the
Database.query()
call somehow has access to it.String queryString = 'SELECT Id, Name FROM Account'; if(true) { // accountNames variable is not available outside the `if` statement Set<String> accountNames = new Set<String> {'Test Account'}; queryString += ' WHERE Name IN :accountNames'; } // this debug will fail if you uncomment it // system.debug(accountNames); // however the query somehow has access to it. // create an account called Test Account to prove it. system.debug('queryString: ' + queryString); system.debug('queryString result: ' + database.query(queryString));
But, Salesforce is smart enough to know that it’s not just some random string, so the following will fail:
// Alternatively, use a random string as the variable name, and salesforce // is smart enough to know it's gibberish String queryString2 = 'SELECT Id, Name FROM Account'; if(true) { queryString2 += ' WHERE Name IN :randomString'; } try { system.debug('queryString2 result: ' + database.query(queryString2)); system.assert(false); } catch (QueryException qe) { system.debug(':randomString is gibberish: ' + qe.getMessage()); }
Edit:
Interestingly, the
:randomString
is definitely calculated only when the query is executed.String queryString3 = 'SELECT Id, Name FROM Account'; String queryString4 = 'SELECT Id, Name FROM Account'; if(true) { // assign to query 3 before it has a value queryString3 += ' WHERE Name IN :randomString'; // assign to query 4 after it has been declared Set<String> randomString = new Set<String>{'Test Account'}; queryString4 += ' WHERE Name IN :randomString'; } // both queries return correct results, regardless of when the variable was declared system.debug('queryString3 result: ' + database.query(queryString3)); system.debug('queryString4 result: ' + database.query(queryString4));
But it’s not an open slather. The following throws an error:
String queryString = 'SELECT Id, Name FROM Account WHERE Name = :someField'; someMethod(); system.debug('queryString result: ' + database.query(queryString)); void someMethod() { String someField = 'Test Account'; system.debug('someField: ' + someField); }
Does anyone know why this is the case, or have an explanation why this would work?
As far as I can tell, at the time that the
accountNames
variable is in scope, no part of the code knows that it’s anything other than a string.
Answer
Conjecture
This seems like an issue with the garbage collector internals, as if exiting the block scope created by if
/do
/for
, etc does not cause the garbage collector to run and clear out heap variables that are no longer visible. You’ll have to open a case to find anything more definite.
Evidence (?)
I ran the following and consider it of interest:
system.debug(Limits.getHeapSize()); // 1047
if (true)
{
List<Integer> values = new List<Integer>();
for (Integer i = 0; i < 1000000; i++) values.add(i);
}
system.debug(Limits.getHeapSize()); // 12001063
The following results in a negligible reduction in heap size (13 bytes), probably because i
gets collected:
// external class
public static List<Integer> getValues()
{
List<Integer> values = new List<Integer>();
for (Integer i = 0; i < 1000000; i++) values.add(i);
return values;
}
// execute anonymous
system.debug(Limits.getHeapSize()); // 1045
if (true)
{
List<Integer> values = MyClass.getValues();
}
system.debug(Limits.getHeapSize()); // 12001050
The following seems to indicate METHOD_EXIT
calls the garbage collector:
system.debug(Limits.getHeapSize()); // 1045
if (true)
{
Throwaway.getValues();
}
system.debug(Limits.getHeapSize()); // 1046
Experimentation
I tried to see if it is a timing issue with the following:
String queryString = 'SELECT Id, Name FROM Account';
if(true)
{
Set<String> accountNames = new Set<String> {'Test'};
queryString += ' WHERE Name IN :accountNames';
}
for (Integer i=0; i<1000000; i++) String.isBlank('abc123');
system.debug('queryString result: ' + database.query(queryString));
Still works.
It doesn’t seem related to how you’re switching context either.
do
{
Set<String> accountNames = new Set<String> {'Test'};
queryString += ' WHERE Name IN :accountNames';
}
while (false);
Still works.
Context Switching
Passing the query string to a @future
will fail. The following Batchable
will find the right batches, but fail on the execute
block. It will also fail on the finish
block if you comment the execute
body.
public class Throwaway implements Database.Batchable<SObject>
{
String query;
public Throwaway()
{
query = 'SELECT Id FROM Account WHERE Name = :hiddenValue';
if (true) { String hiddenValue = 'Test'; }
}
public Database.QueryLocator start(Database.BatchableContext bc)
{
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext bc, List<SObject> records)
{
system.debug(Database.query(query));
}
public void finish(Database.BatchableContext bc)
{
system.debug(Database.query(query));
}
}
Attribution
Source : Link , Question Author : Nick Cook , Answer Author : Adrian Larson