diff --git a/fflib/src/classes/fflib_QueryFactory.cls b/fflib/src/classes/fflib_QueryFactory.cls index adcccfc9b28..c23c74d2113 100644 --- a/fflib/src/classes/fflib_QueryFactory.cls +++ b/fflib/src/classes/fflib_QueryFactory.cls @@ -61,7 +61,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr **/ public Schema.SObjectType table {get; private set;} @testVisible - private Set fields; + private List fields; private String conditionExpression; private Integer limitCount; private Integer offset; @@ -71,9 +71,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr /* This can optionally be enforced (or not) by calling the setEnforceFLS method prior to calling /* one of the selectField or selectFieldset methods. **/ - private Boolean enforceFLS; - - private Boolean sortSelectFields = true; + @TestVisible + private Boolean enforceFLS = false; + @TestVisible + private Boolean lightweight = false; /** * The relationship and subselectQueryMap variables are used to support subselect queries. Subselects can be added to @@ -85,6 +86,12 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr private Map subselectQueryMap; private QueryField getFieldToken(String fieldName){ + + // FLS will not be enforced, so we are going to take a lot of shortcuts in the name of performance + if (this.lightweight) { + return new LightweightQueryField(fieldName); + } + QueryField result; if(!fieldName.contains('.')){ //single field Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase()); @@ -130,19 +137,31 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr return false; return ((fflib_QueryFactory)obj).toSOQL() == this.toSOQL(); } - + /** * Construct a new fflib_QueryFactory instance with no options other than the FROM caluse. * You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query. * @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}. **/ public fflib_QueryFactory(Schema.SObjectType table){ - this.table = table; - fields = new Set(); - order = new List(); - enforceFLS = false; + this(table, false); } + /** + * Construct a new fflib_QueryFactory instance, allowing you to use LightweightQueryFields + * to build the query. This offers significant performance improvement in query build time + * at the expense of FLS enforcement, and up-front field validation. + * @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}. + * @param lightweight a Boolean that specifies whether the LightweightQueryField is to be used when building the query. + **/ + public fflib_QueryFactory(Schema.SObjectType table, Boolean lightweight) { + this.table = table; + this.fields = new List(); + this.order = new List(); + this.lightweight = lightweight; + this.enforceFLS = false; + } + /** * Construct a new fflib_QueryFactory instance with no options other than the FROM clause and the relationship. * This should be used when constructing a subquery query for addition to a parent query. @@ -151,10 +170,22 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr * @param relationship the ChildRelationship to be used in the FROM Clause of the resultant Query (when set overrides value of table). This sets the value of {@link #relationship} and {@link #table}. **/ private fflib_QueryFactory(Schema.ChildRelationship relationship){ - this(relationship.getChildSObject()); + this(relationship, false); + } + + /** + * Construct a new fflib_QueryFactory instance with no options other than the FROM clause and the relationship. + * This should be used when constructing a subquery query for addition to a parent query. + * Objects created with this constructor cannot be added to another object using the subselectQuery method. + * You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query. + * @param relationship the ChildRelationship to be used in the FROM Clause of the resultant Query (when set overrides value of table). This sets the value of {@link #relationship} and {@link #table}. + * @param lightweight a Boolean that specifies whether the LightweightQueryField is to be used when building the query. + **/ + private fflib_QueryFactory(Schema.ChildRelationship relationship, Boolean lightweight){ + this(relationship.getChildSObject(), lightweight); this.relationship = relationship; } - + /** * This method checks to see if the User has Read Access on {@link #table}. * Asserts true if User has access. @@ -171,6 +202,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr * @param enforce whether to enforce field level security (read) **/ public fflib_QueryFactory setEnforceFLS(Boolean enforce){ + if (this.lightweight && enforce) { + throw new InvalidOperationException('Calling setEnforceFLS(true) on a "lightweight" QueryFactory instance is not allowed.'); + } this.enforceFLS = enforce; return this; } @@ -179,10 +213,10 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr * Sets a flag to indicate that this query should have ordered * query fields in the select statement (this at a small cost to performance). * If you are processing large query sets, you should switch this off. + * @deprecated Fields are ALWAYS sorted within the generated SOQL, so this method now does nothing. * @param whether or not select fields should be sorted in the soql statement. **/ public fflib_QueryFactory setSortSelectFields(Boolean doSort){ - this.sortSelectFields = doSort; return this; } @@ -192,7 +226,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr * @param fieldName the API name of the field to add to the query's SELECT clause. **/ public fflib_QueryFactory selectField(String fieldName){ - fields.add( getFieldToken(fieldName) ); + this.fields.add( getFieldToken(fieldName) ); return this; } /** @@ -206,20 +240,32 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr throw new InvalidFieldException(null,this.table); if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, field); - fields.add( new QueryField(field) ); + this.fields.add(getQueryFieldFromToken(field)); return this; } + + /** + * Returns the appropriate QueryField implementation, based on the "lightweight" flag + * @param field the {@link Schema.SObjectField} for the QueryField + * @returns either a QueryField, or LightweightQueryField object for the specified SObjectField + **/ + private QueryField getQueryFieldFromToken(Schema.SObjectField field) { + QueryField qf; + if (this.lightweight) + qf = new LightweightQueryField(field); + else + qf = new QueryField(field); + return qf; + } + /** * Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times. * @param fieldNames the Set of field API names to select. **/ public fflib_QueryFactory selectFields(Set fieldNames){ - List fieldList = new List(); - Set toAdd = new Set(); for(String fieldName:fieldNames){ - toAdd.add( getFieldToken(fieldName) ); - } - fields.addAll(toAdd); + this.fields.add( getFieldToken(fieldName) ); + } return this; } /** @@ -227,10 +273,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr * @param fieldNames the List of field API names to select. **/ public fflib_QueryFactory selectFields(List fieldNames){ - Set toAdd = new Set(); for(String fieldName:fieldNames) - toAdd.add( getFieldToken(fieldName) ); - fields.addAll(toAdd); + this.fields.add( getFieldToken(fieldName) ); return this; } /** @@ -243,8 +287,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr if(token == null) throw new InvalidFieldException(); if (enforceFLS) - fflib_SecurityUtils.checkFieldIsReadable(table, token); - this.fields.add( new QueryField(token) ); + fflib_SecurityUtils.checkFieldIsReadable(table, token); + this.fields.add(getQueryFieldFromToken(token)); } return this; } @@ -258,8 +302,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr if(token == null) throw new InvalidFieldException(); if (enforceFLS) - fflib_SecurityUtils.checkFieldIsReadable(table, token); - this.fields.add( new QueryField(token) ); + fflib_SecurityUtils.checkFieldIsReadable(table, token); + this.fields.add(getQueryFieldFromToken(token)); } return this; } @@ -326,12 +370,13 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr } /** + * @deprecated Replaced by {@link #getSelectedFieldsAsList()} * @returns the selected fields **/ public Set getSelectedFields() { - return this.fields; + return new Set(this.fields); } - + /** * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned. * If not, a new one will be created and returned. @@ -420,11 +465,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr return subselectQueryMap.get(relationship); } - fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship); + fflib_QueryFactory subselectQuery = new fflib_QueryFactory(relationship, this.lightweight); - //The child queryFactory should be configured in the same way as the parent by default - can override after if required - subSelectQuery.setSortSelectFields(sortSelectFields); - if(assertIsAccessible){ subSelectQuery.assertIsAccessible(); } @@ -501,7 +543,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr **/ public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction, Boolean nullsLast){ order.add( - new Ordering(new QueryField(field), direction, nullsLast) + new Ordering(getQueryFieldFromToken(field), direction, nullsLast) ); return this; } @@ -539,7 +581,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr **/ public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction){ order.add( - new Ordering(new QueryField(field), direction) + new Ordering(getQueryFieldFromToken(field), direction) ); return this; } @@ -554,15 +596,20 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr if (fields.size() == 0){ if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, 'Id'); result += 'Id '; - }else if(sortSelectFields){ - List fieldsToQuery = new List(fields); - fieldsToQuery.sort(); //delegates to QueryFilter's comparable implementation - for(QueryField field:fieldsToQuery){ - result += field + ', '; + } else { + // This bit of code de-dupes the list of QueryFields. Since we've moved away from using a Set to back this collection + // (for performance reasons related to https://github.com/financialforcedev/fflib-apex-common/issues/79), we de-dupe + // by first sorting the List of QueryField objects (in order of the String representation of the field path), + // then making a pass through the list leaving dupes out of the "fieldsToQuery" collection. + fields.sort(); // Sorts based on QueryFields's "comparable" implementation + // Now that the QueryField list is sorted, we can de-dupe + QueryField previousQf = null; + for(QueryField field : fields){ + if (!field.equals(previousQf)) { + result += field + ', '; + } + previousQf = field; } - }else{ - for (QueryField field : fields) - result += field + ', '; } if(subselectQueryMap != null && !subselectQueryMap.isEmpty()){ @@ -592,7 +639,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr **/ public fflib_QueryFactory deepClone(){ - fflib_QueryFactory clone = new fflib_QueryFactory(this.table) + fflib_QueryFactory clone = new fflib_QueryFactory(this.table, this.lightweight) .setLimit(this.limitCount) .setCondition(this.conditionExpression) .setEnforceFLS(this.enforceFLS); @@ -664,8 +711,54 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr } } + public class LightweightQueryField extends QueryField implements Comparable { + String fieldName; + + private LightweightQueryField() {} + + @TestVisible + private LightweightQueryField(String fieldName) { + // Convert strings to lowercase so they sort in a case insensitive manner + this.fieldName = fieldName.toLowercase(); + } + + @TestVisible + private LightweightQueryField(Schema.SObjectField field) { + // Convert strings to lowercase so they sort in a case insensitive manner + this.fieldName = field.getDescribe().getLocalName().toLowercase(); + } + + public override String toString() { return this.fieldName; } + + public override Integer hashCode() { + return (this.fieldName == null) ? 0 : this.fieldName.hashCode(); + } + + public override Boolean equals(Object obj) { + return ((obj != null) + && (obj instanceof LightweightQueryField) + && (this.fieldName == ((LightweightQueryField) obj).fieldName)); + } + + public override Integer compareTo(Object obj) { + if (obj == null || !(obj instanceof LightweightQueryField)) + return 1; + + if (this.fieldName == null) { + if (((LightweightQueryField) obj).fieldName == null) + // Both objects are non-null, but their fieldName is null + return 0; + else + // Our fieldName is null, but theirs isn't + return -1; + } + + // Both objects have non-null fieldNames, so just return the result of String.compareTo + return this.fieldName.compareTo(((LightweightQueryField) obj).fieldName); + } + } - public class QueryField implements Comparable{ + public virtual class QueryField implements Comparable{ List fields; /** @@ -681,7 +774,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr public List getFieldPath(){ return fields.clone(); } - + + private QueryField() {} + @testVisible private QueryField(List fields){ if(fields == null || fields.size() == 0) @@ -694,7 +789,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr throw new InvalidFieldException('Invalid field: null'); fields = new List{ field }; } - public override String toString(){ + public virtual override String toString(){ String result = ''; Integer size = fields.size(); for (Integer i=0; i{'acCounTId', 'account.name'} ); qf.selectFields( new List{'homePhonE','fAX'} ); qf.selectFields( new List{ Contact.Email, Contact.Title } ); + // QueryField weights foreign key fields differently, so account.name will be last + System.assertEquals('select accountid, email, fax, firstname, homephone, lastname, title, account.name from contact', qf.toSOQL().toLowercase()); + + // Test the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('firstName'); + qf.selectField(Schema.Contact.SObjectType.fields.lastName); + qf.selectFields( new Set{'acCounTId', 'account.name'} ); + qf.selectFields( new List{'homePhonE','fAX'} ); + qf.selectFields( new List{ Contact.Email, Contact.Title } ); + // LightweightQueryField converts all field paths to lowercase and sorts in alphabetical order, so account.name will be first + System.assertEquals('select account.name, accountid, email, fax, firstname, homephone, lastname, title from contact', qf.toSOQL().toLowercase()); } @isTest @@ -47,6 +59,15 @@ private class fflib_QueryFactoryTest { qf.setLimit(100); System.assertEquals(100,qf.getLimit()); System.assert( qf.toSOQL().endsWithIgnoreCase('LIMIT '+qf.getLimit()), 'Failed to respect limit clause:'+qf.toSOQL() ); + + // Test the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('NAMe').selectFields( new Set{'naMe', 'email'}); + query = qf.toSOQL().toLowercase(); + System.assert( Pattern.matches('select.*email,.*name.*from.*',query), 'Expected email and name fields in query, got '+query); + qf.setLimit(100); + System.assertEquals(100,qf.getLimit()); + System.assert(qf.toSOQL().endsWithIgnoreCase('limit '+qf.getLimit()), 'Failed to respect limit clause:'+qf.toSOQL()); } @isTest @@ -59,14 +80,33 @@ private class fflib_QueryFactoryTest { System.assertEquals(whereClause,qf.getCondition()); String query = qf.toSOQL(); System.assert(query.endsWith('WHERE name = \'test\''),'Query should have ended with a filter on name, got: '+query); + + // Test the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name'); + qf.selectField('email'); + qf.setCondition( whereClause ); + System.assertEquals(whereClause,qf.getCondition()); + query = qf.toSOQL(); + System.assert(query.endsWithIgnoreCase('where name = \'test\''),'Query should have ended with a filter on name, got: '+query); } @isTest static void duplicateFieldSelection() { fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); qf.selectField('NAMe').selectFields( new Set{'naMe', 'email'}); - String query = qf.toSOQL(); - System.assertEquals(1, query.countMatches('Name'), 'Expected one name field in query: '+query ); + String query = qf.toSOQL().toLowercase(); + // The resulting query will be de-duped so that only one "Name" field exists in the generated query + System.assertEquals(1, query.countMatches('name'), 'Expected one name field in query: '+query ); + System.assertEquals(1, query.countMatches('email'), 'Expected one email field in query: '+query ); + + // Test the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType); + qf.selectField('NAMe').selectFields( new Set{'naMe', 'email'}); + query = qf.toSOQL().toLowercase(); + // The resulting query will be de-duped so that only one "Name" field exists in the generated query + System.assertEquals(1, query.countMatches('name'), 'Expected one name field in query: '+query ); + System.assertEquals(1, query.countMatches('email'), 'Expected one email field in query: '+query ); } @isTest @@ -78,7 +118,20 @@ private class fflib_QueryFactoryTest { System.assertNotEquals(qf1,qf2); qf2.selectField('NAmE'); System.assertEquals(qf1,qf2); - qf1.selectField('name').selectFields( new Set{ 'NAME', 'name' }).selectFields( new Set{ Contact.Name, Contact.Name} ); + qf1.selectFields( new Set{ 'NAME' }); + qf2.selectFields( new Set{ Contact.Name} ); + System.assertEquals(qf1,qf2); + + // Test the LightweightQueryField implementation + qf1 = new fflib_QueryFactory(Contact.SObjectType, true); + qf2 = new fflib_QueryFactory(Contact.SObjectType, true); + System.assertEquals(qf1,qf2); + qf1.selectField('name'); + System.assertNotEquals(qf1,qf2); + qf2.selectField('NAmE'); + System.assertEquals(qf1,qf2); + qf1.selectFields( new Set{ 'NAME' }); + qf2.selectFields( new Set{ Contact.Name} ); System.assertEquals(qf1,qf2); } @@ -146,9 +199,22 @@ private class fflib_QueryFactoryTest { System.assertEquals(Contact.name,qf.getOrderings()[0].getField() ); System.assertEquals(fflib_QueryFactory.SortOrder.DESCENDING,qf.getOrderings()[1].getDirection() ); - System.assert( Pattern.matches('SELECT.*Name.*FROM.*',query), 'Expected Name field in query, got '+query); System.assert( Pattern.matches('SELECT.*Email.*FROM.*',query), 'Expected Name field in query, got '+query); + + // Equivalent test for LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name'); + qf.selectField('email'); + qf.setCondition( 'name = \'test\'' ); + qf.addOrdering( new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ).addOrdering( new fflib_QueryFactory.Ordering('Contact','CreatedDATE',fflib_QueryFactory.SortOrder.DESCENDING) ); + query = qf.toSOQL().toLowerCase(); + + System.assertEquals(2,qf.getOrderings().size()); + System.assertEquals(Contact.name,qf.getOrderings()[0].getField() ); + System.assertEquals(fflib_QueryFactory.SortOrder.DESCENDING,qf.getOrderings()[1].getDirection() ); + + System.assert( Pattern.matches('select.*email,.*name.*from.*',query), 'Expected name and email fields in query, got '+query); } @isTest @@ -256,7 +322,7 @@ private class fflib_QueryFactoryTest { Schema.Contact.SObjectType.fields.AccountId}); System.assert(!qfld.equals(new Contact())); } - + @isTest static void addChildQueriesWithChildRelationship_success(){ Account acct = new Account(); @@ -275,7 +341,6 @@ private class fflib_QueryFactoryTest { fflib_QueryFactory qf = new fflib_QueryFactory(Contact.SObjectType); qf.selectField('name').selectField('Id').setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING, true); - Schema.DescribeSObjectResult descResult = Contact.SObjectType.getDescribe(); //explicitly assert object accessibility when creating the subselect qf.subselectQuery('Tasks', true).selectField('Id').selectField('Subject').setCondition(' IsDeleted = false '); List queries = qf.getSubselectQueries(); @@ -284,8 +349,20 @@ private class fflib_QueryFactoryTest { System.assert(contacts != null && contacts.size() == 1); System.assert(contacts[0].Tasks.size() == 1); System.assert(contacts[0].Tasks[0].Subject == 'test'); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name').selectField('Id').setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING, true); + //explicitly assert object accessibility when creating the subselect + qf.subselectQuery('Tasks', true).selectField('Id').selectField('Subject').setCondition(' IsDeleted = false '); + queries = qf.getSubselectQueries(); + System.assert(queries != null); + contacts = Database.query(qf.toSOQL()); + System.assert(contacts != null && contacts.size() == 1); + System.assert(contacts[0].Tasks.size() == 1); + System.assert(contacts[0].Tasks[0].Subject == 'test'); } - + @isTest static void addChildQueriesWithChildRelationshipNoAccessibleCheck_success(){ Account acct = new Account(); @@ -312,6 +389,18 @@ private class fflib_QueryFactoryTest { System.assert(contacts != null && contacts.size() == 1); System.assert(contacts[0].Tasks.size() == 1); System.assert(contacts[0].Tasks[0].Subject == 'test'); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name').selectField('Id').setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING, true); + //explicitly assert object accessibility when creating the subselect + qf.subselectQuery('Tasks').selectField('Id').selectField('Subject').setCondition(' IsDeleted = false '); + queries = qf.getSubselectQueries(); + System.assert(queries != null); + contacts = Database.query(qf.toSOQL()); + System.assert(contacts != null && contacts.size() == 1); + System.assert(contacts[0].Tasks.size() == 1); + System.assert(contacts[0].Tasks[0].Subject == 'test'); } @isTest @@ -349,6 +438,17 @@ private class fflib_QueryFactoryTest { System.assert(contacts != null && contacts.size() == 1); System.assert(contacts[0].Tasks.size() == 1); System.assert(contacts[0].Tasks[0].Subject == 'test'); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name').selectField('Id').setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING, true); + qf.subselectQuery(relationship, true).selectField('Id').selectField('Subject').setCondition(' IsDeleted = false '); + queries = qf.getSubselectQueries(); + System.assert(queries != null); + contacts = Database.query(qf.toSOQL()); + System.assert(contacts != null && contacts.size() == 1); + System.assert(contacts[0].Tasks.size() == 1); + System.assert(contacts[0].Tasks[0].Subject == 'test'); } @isTest @@ -386,6 +486,17 @@ private class fflib_QueryFactoryTest { System.assert(contacts != null && contacts.size() == 1); System.assert(contacts[0].Tasks.size() == 1); System.assert(contacts[0].Tasks[0].Subject == 'test'); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name').selectField('Id').setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING, true); + qf.subselectQuery(relationship).selectField('Id').selectField('Subject').setCondition(' IsDeleted = false '); + queries = qf.getSubselectQueries(); + System.assert(queries != null); + contacts = Database.query(qf.toSOQL()); + System.assert(contacts != null && contacts.size() == 1); + System.assert(contacts[0].Tasks.size() == 1); + System.assert(contacts[0].Tasks[0].Subject == 'test'); } @isTest @@ -416,6 +527,17 @@ private class fflib_QueryFactoryTest { e = ex; } System.assertNotEquals(e, null); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name').selectField('Id').setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING, true); + e = null; + try { + qf.subselectQuery('Tas').selectField('Id').selectField('Subject').setCondition(' IsDeleted = false '); + } catch (fflib_QueryFactory.InvalidSubqueryRelationshipException ex) { + e = ex; + } + System.assertNotEquals(e, null); } @isTest @@ -445,6 +567,17 @@ private class fflib_QueryFactoryTest { System.assert(contacts != null && contacts.size() == 1); System.assert(contacts[0].Tasks.size() == 1); System.assert(contacts[0].Tasks[0].Subject == 'test'); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name').selectField('Id').setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING, true); + qf.subselectQuery(Task.SObjectType, true).selectField('Id').selectField('Subject').setCondition(' IsDeleted = false '); + queries = qf.getSubselectQueries(); + System.assert(queries != null); + contacts = Database.query(qf.toSOQL()); + System.assert(contacts != null && contacts.size() == 1); + System.assert(contacts[0].Tasks.size() == 1); + System.assert(contacts[0].Tasks[0].Subject == 'test'); } @isTest @@ -483,6 +616,21 @@ private class fflib_QueryFactoryTest { List queries = qf.getSubselectQueries(); System.assert(queries != null); System.assert(queries.size() == 1); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name'); + qf.selectField('Id'); + qf.setCondition( 'name like \'%test%\'' ); + qf.addOrdering( new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ).addOrdering('CreatedBy.Name',fflib_QueryFactory.SortOrder.DESCENDING); + System.assert(qf.getSubselectQueries() == null); + childQf = qf.subselectQuery(Task.SObjectType); + childQf.assertIsAccessible(); + childQf.selectField('Id'); + childQf2 = qf.subselectQuery(Task.SObjectType); + queries = qf.getSubselectQueries(); + System.assert(queries != null); + System.assert(queries.size() == 1); } @isTest @@ -509,6 +657,21 @@ private class fflib_QueryFactoryTest { e = ex; } System.assertNotEquals(e, null); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name'); + qf.selectField('email'); + qf.setCondition( 'name like \'%test%\'' ); + qf.addOrdering( new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ).addOrdering( 'CreatedDATE',fflib_QueryFactory.SortOrder.DESCENDING); + e = null; + try { + fflib_QueryFactory childQf = qf.subselectQuery(Contact.SObjectType); + childQf.selectField('Id'); + } catch (fflib_QueryFactory.InvalidSubqueryRelationshipException ex) { + e = ex; + } + System.assertNotEquals(e, null); } @isTest @@ -538,6 +701,24 @@ private class fflib_QueryFactoryTest { e = ex; } System.assertNotEquals(e, null); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.selectField('name'); + qf.selectField('email'); + qf.setCondition( 'name like \'%test%\'' ); + qf.addOrdering( new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ).addOrdering('CreatedDATE',fflib_QueryFactory.SortOrder.DESCENDING); + + childQf = qf.subselectQuery(Task.SObjectType); + childQf.selectField('Id'); + childQf.selectField('Subject'); + e = null; + try { + fflib_QueryFactory subChildQf = childQf.subselectQuery(Task.SObjectType); + } catch (fflib_QueryFactory.InvalidSubqueryRelationshipException ex) { + e = ex; + } + System.assertNotEquals(e, null); } @isTest @@ -566,6 +747,22 @@ private class fflib_QueryFactoryTest { break; } System.assert(qf.toSOQL().containsIgnoreCase('NULLS LAST')); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.assertIsAccessible() + .selectField('createdby.name') + .selectField(Contact.LastModifiedById) + .selectFields(new List{Contact.LastModifiedDate}) + .selectField(Contact.LastName) + .selectFields(new List{Contact.Id}) + .setCondition( 'name like \'%test%\'' ) + .selectFields(new Set{Contact.FirstName}) + .addOrdering(new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ) + .addOrdering(Contact.LastModifiedDate,fflib_QueryFactory.SortOrder.DESCENDING) + .addOrdering(Contact.CreatedDate,fflib_QueryFactory.SortOrder.DESCENDING, true); + String soql = qf.toSOQL(); + System.assert(soql.containsIgnoreCase('name asc nulls first , lastmodifieddate desc nulls first , createddate desc nulls last'), 'Query did not contain the expected orderings: ' + soql); } @isTest @@ -583,7 +780,18 @@ private class fflib_QueryFactoryTest { excThrown = true; } System.assert(excThrown); - } + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Account.SObjectType, true); + excThrown = false; + try { + //check to see if this record is accessible, it isn't. + qf.assertIsAccessible(); + } catch (fflib_SecurityUtils.CrudException e) { + excThrown = true; + } + System.assert(excThrown); + } } } @@ -603,6 +811,8 @@ private class fflib_QueryFactoryTest { excThrown = true; } System.assert(excThrown); + + // No equivalent test for Lightweight implementation because it doesn't enforce FLS } } } @@ -613,6 +823,12 @@ private class fflib_QueryFactoryTest { qf.assertIsAccessible().setEnforceFLS(true).setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING); String query = qf.toSOQL(); System.assert(query.containsIgnoreCase('Id FROM')); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true); + qf.assertIsAccessible().setCondition( 'name like \'%test%\'' ).addOrdering('CreatedDate',fflib_QueryFactory.SortOrder.DESCENDING); + query = qf.toSOQL(); + System.assert(query.containsIgnoreCase('Id FROM')); } @isTest @@ -642,7 +858,30 @@ private class fflib_QueryFactoryTest { System.assert(joinQf.compareTo(otherJoinQf) > 0); System.assert(otherJoinQf.compareTo(joinQf) < 0); } - + + @isTest + static void lightweightQueryField_comparison() { + String otherType = 'bob'; + fflib_QueryFactory.QueryField qf = new fflib_QueryFactory.LightweightQueryField(Contact.SObjectType.fields.Name); + fflib_QueryFactory.QueryField equivalentQf = new fflib_QueryFactory.LightweightQueryField('name'); + fflib_QueryFactory.QueryField lesserQf = new fflib_QueryFactory.LightweightQueryField(Contact.SObjectType.fields.AccountId); + fflib_QueryFactory.QueryField greaterQf = new fflib_QueryFactory.LightweightQueryField(Contact.SObjectType.fields.SystemModstamp); + System.assert(qf.compareTo(otherType) > 0); + System.assert(!qf.equals(otherType)); + System.assert(qf.compareTo(null) > 0); + System.assert(!qf.equals(null)); + System.assertEquals(0, qf.compareTo(qf)); + System.assert(qf.equals(qf)); + System.assertEquals(0, qf.compareTo(equivalentQf)); + System.assertEquals(0, equivalentQf.compareTo(qf)); + System.assert(qf.equals(equivalentQf)); + System.assert(qf.compareTo(lesserQf) > 0); + System.assert(lesserQf.compareTo(qf) < 0); + System.assert(!qf.equals(lesserQf)); + System.assert(qf.compareTo(greaterQf) < 0); + System.assert(!qf.equals(greaterQf)); + } + @isTest static void deterministic_toSOQL(){ fflib_QueryFactory qf1 = new fflib_QueryFactory(User.SObjectType); @@ -665,6 +904,25 @@ private class fflib_QueryFactoryTest { System.assertEquals(qf1.toSOQL(), qf2.toSOQL()); System.assertEquals(expectedQuery, qf1.toSOQL()); System.assertEquals(expectedQuery, qf2.toSOQL()); + + // Try the same test using the LightweightQueryField implementation + qf1 = new fflib_QueryFactory(User.SObjectType, true); + qf2 = new fflib_QueryFactory(User.SObjectType, true); + for(fflib_QueryFactory qf:new Set{qf1,qf2}){ + qf.selectFields(new List{ + 'Id', + 'FirstName', + 'LastName', + 'CreatedBy.Name', + 'CreatedBy.Manager', + 'LastModifiedBy.Email' + }); + } + // The sort order will be different. It is based solely on alphabetical order + expectedQuery = 'select createdby.manager, createdby.name, firstname, id, lastmodifiedby.email, lastname from user'; + System.assertEquals(qf1.toSOQL(), qf2.toSOQL()); + System.assertEquals(expectedQuery, qf1.toSOQL().toLowercase()); + System.assertEquals(expectedQuery, qf2.toSOQL().toLowercase()); } @isTest @@ -685,6 +943,31 @@ private class fflib_QueryFactoryTest { System.assertEquals(qf.getCondition(), qf2.getCondition()); System.assertEquals(qf.toSOQL(), qf2.toSOQL()); System.assertEquals(qf.getOrderings(), qf2.getOrderings()); + System.assertEquals(qf.enforceFLS, qf2.enforceFLS); + System.assertEquals(qf.lightweight, qf2.lightweight); + System.assert(qf.enforceFLS); + System.assert(!qf.lightweight); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Contact.SObjectType, true) + .setLimit(10) + .setCondition('id=12345') + .selectField('Description') + .addOrdering(new fflib_QueryFactory.Ordering('Contact','name',fflib_QueryFactory.SortOrder.ASCENDING) ) + .addOrdering( new fflib_QueryFactory.Ordering('Contact','CreatedDATE',fflib_QueryFactory.SortOrder.DESCENDING)); + + qf2 = qf.deepClone(); + + System.assertEquals(qf2, qf); + + System.assertEquals(qf.getLimit(), qf2.getLimit()); + System.assertEquals(qf.getCondition(), qf2.getCondition()); + System.assertEquals(qf.toSOQL(), qf2.toSOQL()); + System.assertEquals(qf.getOrderings(), qf2.getOrderings()); + System.assertEquals(qf.enforceFLS, qf2.enforceFLS); + System.assertEquals(qf.lightweight, qf2.lightweight); + System.assert(!qf.enforceFLS); + System.assert(qf.lightweight); } @isTest @@ -708,6 +991,30 @@ private class fflib_QueryFactoryTest { System.assertEquals(qf.toSOQL(), qf2.toSOQL()); System.assertEquals(qf.getOrderings(), qf2.getOrderings()); System.assertEquals(qf.getSubselectQueries(), qf2.getSubselectQueries()); + System.assert(qf.enforceFLS); + System.assert(!qf.lightweight); + + // Try the same test using the LightweightQueryField implementation + qf = new fflib_QueryFactory(Account.SObjectType, true) + .setLimit(10) + .setCondition('id=12345') + .selectField('Description') + .addOrdering(new fflib_QueryFactory.Ordering('Account','Name',fflib_QueryFactory.SortOrder.ASCENDING) ) + .addOrdering( new fflib_QueryFactory.Ordering('Account','Description',fflib_QueryFactory.SortOrder.DESCENDING)); + + qf.subselectQuery('Contacts', true); + + qf2 = qf.deepClone(); + + System.assertEquals(qf, qf2); + + System.assertEquals(qf.getLimit(), qf2.getLimit()); + System.assertEquals(qf.getCondition(), qf2.getCondition()); + System.assertEquals(qf.toSOQL(), qf2.toSOQL()); + System.assertEquals(qf.getOrderings(), qf2.getOrderings()); + System.assertEquals(qf.getSubselectQueries(), qf2.getSubselectQueries()); + System.assert(!qf.enforceFLS); + System.assert(qf.lightweight); } @isTest @@ -777,34 +1084,7 @@ private class fflib_QueryFactoryTest { } @isTest - static void testSoql_unsortedSelectFields(){ - //Given - fflib_QueryFactory qf = new fflib_QueryFactory(User.SObjectType); - qf.selectFields(new List{ - 'Id', - 'FirstName', - 'LastName', - 'CreatedBy.Name', - 'CreatedBy.Manager', - 'LastModifiedBy.Email' - }); - - qf.setSortSelectFields(false); - - String orderedQuery = - 'SELECT ' - +'FirstName, Id, LastName, ' //less joins come first, alphabetically - +'CreatedBy.ManagerId, CreatedBy.Name, LastModifiedBy.Email ' //alphabetical on the same number of joins' - +'FROM User'; - - //When - String actualSoql = qf.toSOQL(); - - //Then - System.assertNotEquals(orderedQuery, actualSoql); - } - - + public static User createTestUser_noAccess(){ User usr; try { diff --git a/fflib/src/classes/fflib_SObjectSelector.cls b/fflib/src/classes/fflib_SObjectSelector.cls index 80b2eb12a11..cd0a5e98033 100644 --- a/fflib/src/classes/fflib_SObjectSelector.cls +++ b/fflib/src/classes/fflib_SObjectSelector.cls @@ -289,6 +289,12 @@ public abstract with sharing class fflib_SObjectSelector return getSObjectType(); } + public fflib_QueryFactory newLightweightQueryFactory() { + return configureQueryFactory( + new fflib_QueryFactory(getSObjectType2(), true), m_enforceCRUD, false, true + ); + } + /** * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by **/