Salesforce Stack Exchange is a question and answer site for Salesforce administrators, implementation experts, developers and anybody in-between. It's 100% free, no registration required.

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

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.

share|improve this question
    
Are you sure there's nowhere else in context that you are defining this variable name? I can reproduce in Execute Anonymous so this is just...weird. – Adrian Larson Mar 5 at 2:31
1  
You've passed accountNames by reference using another variable when you added it to queryString using the +=. – crmprogdev Mar 5 at 2:35
    
@AdrianLarson All I'm using is the posted code in Execute Anonymous. – Nick Cook Mar 5 at 2:40
    
@crmprogdev Not sure I understand. Isn't :accountNames just a string at that point in the code? Or is Salesforce behind the scenes already doing some magic with it at that point? Is there a way to test this? – Nick Cook Mar 5 at 2:42
2  
Definitely a bug. I guess we're going to have to report this. – sfdcfox Mar 5 at 3:25
up vote 3 down vote accepted

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));
    }
}
share|improve this answer
    
Interesting, I ran your heapSize test but placed the for loop in a separate method, and the heapSize at the end doesn't grow. I'd say that's more expected, which points more to your theory on the garbage collector. – Nick Cook Mar 5 at 3:52
    
Which part of the for loop? That's still somewhat of a context switch. All very curious, great question! – Adrian Larson Mar 5 at 3:58
    
I put everything between the two debug(heapSize) statements in a separate method. – Nick Cook Mar 5 at 4:04
    
Seems like METHOD_EXIT may call the garbage collector then. @NickCook – Adrian Larson Mar 5 at 4:06
    
References declared within a method will be on the stack with the stack normally popped at the exit of the method not the exit of a block; the block scoping rules are a language design decision. Garbage collection of the heap is a separate (and infrequent) event. – Keith C Mar 5 at 10:09

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.