Variable scope – database.query() has access to variables not in scope

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

Leave a Comment