From c119866d904c3f10c78a0a2c3b85b16637667abd Mon Sep 17 00:00:00 2001 From: Aaron Rosenzweig Date: Wed, 25 Dec 2013 23:53:41 -0500 Subject: [PATCH] AC-1754 ERXExistsQualifier was doing a global replace of the key in the select statement but should have only replaced once. Without this fix, we cannot reliably chain ExistsQualifiers. Signed-off-by: Aaron Rosenzweig Conflicts: Frameworks/Core/ERExtensions/Sources/er/extensions/eof/qualifiers/ERXExistsQualifier.java --- .../eof/qualifiers/ERXExistsQualifier.java | 284 +++++++++++++++--- 1 file changed, 235 insertions(+), 49 deletions(-) diff --git a/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/qualifiers/ERXExistsQualifier.java b/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/qualifiers/ERXExistsQualifier.java index a0d6928e8d4..e36eca28d68 100644 --- a/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/qualifiers/ERXExistsQualifier.java +++ b/Frameworks/Core/ERExtensions/Sources/er/extensions/eof/qualifiers/ERXExistsQualifier.java @@ -8,11 +8,13 @@ import java.util.Enumeration; +import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EODatabaseContext; import com.webobjects.eoaccess.EOEntity; +import com.webobjects.eoaccess.EOJoin; import com.webobjects.eoaccess.EOProperty; import com.webobjects.eoaccess.EOQualifierSQLGeneration; import com.webobjects.eoaccess.EORelationship; @@ -22,6 +24,8 @@ import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOKeyValueArchiver; import com.webobjects.eocontrol.EOKeyValueArchiving; +import com.webobjects.eocontrol.EOKeyValueCoding; +import com.webobjects.eocontrol.EOKeyValueCodingAdditions; import com.webobjects.eocontrol.EOKeyValueUnarchiver; import com.webobjects.eocontrol.EOObjectStoreCoordinator; import com.webobjects.eocontrol.EOQualifier; @@ -29,9 +33,14 @@ import com.webobjects.foundation.NSCoder; import com.webobjects.foundation.NSCoding; import com.webobjects.foundation.NSDictionary; +import com.webobjects.foundation.NSKeyValueCoding; +import com.webobjects.foundation.NSKeyValueCodingAdditions; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableSet; +import com.webobjects.foundation.NSKeyValueCoding.UnknownKeyException; + +import er.extensions.foundation.ERXArrayUtilities; import er.extensions.foundation.ERXStringUtilities; /** @@ -41,7 +50,7 @@ * * select t0.ID, t0.ATT_1, ... t0.ATT_N from FIRST_TABLE t0 where EXISTS (select t1.ID from ANOTHER_TABLE where t1.ATT_1 = ? and t1.FIRST_TABLE_ID = t0.ID) * - * @author Travis Cripps + * @author Travis Cripps, Aaron Rosenzweig */ public class ERXExistsQualifier extends EOQualifier implements Cloneable, NSCoding, EOKeyValueArchiving { /** @@ -52,6 +61,14 @@ public class ERXExistsQualifier extends EOQualifier implements Cloneable, NSCodi private static final long serialVersionUID = 1L; public static final Logger log = Logger.getLogger(ERXExistsQualifier.class); + public static final String EXISTS_ALIAS = "exists"; + public static final boolean UseSQLInClause = true; + public static final boolean UseSQLExistsClause = false; + + // an EXISTS can be rewritten as an IN and vice versa. Which is faster depends + // on both the database and the data itself. If one is slow for you, try the other + // by flipping this boolean flag. + protected boolean usesInQualInstead = false; /** Register SQL generation support for the qualifier. */ static { @@ -80,12 +97,41 @@ public ERXExistsQualifier(EOQualifier subqualifier) { * @param baseKeyPath to the entity to which the subqualifier will be applied. Note that this should end in a * relationship rather than an attribute, e.g., the key path from an Employee might be department.division. */ - public ERXExistsQualifier(EOQualifier subqualifier, String baseKeyPath) { + public ERXExistsQualifier(EOQualifier subqualifier, String baseKeyPath) { super(); - this.subqualifier = subqualifier; - this.baseKeyPath = baseKeyPath; + /* + * HACK ALERT!! ERXExistsQualifier is broken when passed a keypath. It + * would compare the PK of the baseTable to the PK of the related table. + * I was not able to figure out how to modify the existing logic with + * any amount of confidence that it wouldn't somehow break in some other + * way. This recursion "fixes" the problem by creating Multiple nested + * "in" clauses in the SQL code, which is not the most elegant, readable + * or likely efficient SQL. + */ + if (baseKeyPath != null && baseKeyPath.contains(EOKeyValueCodingAdditions.KeyPathSeparator)) { + String tailPath = ERXStringUtilities.keyPathWithoutFirstProperty(baseKeyPath); + subqualifier = new ERXExistsQualifier(subqualifier, tailPath, UseSQLInClause); // must use "in" clause otherwise sub-select table aliases (exists0) collide + this.subqualifier = subqualifier; // use the new "nested" ERXExistsQualifier + this.baseKeyPath = ERXStringUtilities.firstPropertyKeyInKeyPath(baseKeyPath); + } + else { + this.subqualifier = subqualifier; + this.baseKeyPath = baseKeyPath; + } } + /** + * Public three argument constructor. Use this constructor when you want to try converting the EXISTS into an IN clause + * @param subqualifier sub qualifier + * @param baseKeyPath to the entity to which the subqualifier will be applied. Note that this should end in a + * relationship rather than an attribute, e.g., the key path from an Employee might be department.division. + * @param usesInQualInstead when true will convert the EXISTS clause into an IN clause - to be used if it makes the query plan faster. + */ + public ERXExistsQualifier(EOQualifier subqualifier, String baseKeyPath, boolean usesInQualInstead) { + this(subqualifier, baseKeyPath); + setUsesInQualInstead(usesInQualInstead); + } + /** * Gets the subqualifier that will be applied in the exists clause. * @return the subqualifier @@ -185,24 +231,29 @@ public String sqlStringForSQLExpression(EOQualifier qualifier, EOSQLExpression e String sourceTableAlias = "t0"; // The alias for the the source table of the baseKeyPath from the main query. String destTableAlias; // The alias for the table used in the subquery. if (!srcEntity.equals(baseEntity)) { // The exists clause is applied to the different table. - String sourceKeyPath = ERXStringUtilities.keyPathWithoutLastProperty(baseKeyPath); - sqlStringForAttributeNamedInExpression(sourceKeyPath, expression); sqlStringForAttributeNamedInExpression(baseKeyPath, expression); - sourceTableAlias = (String)expression.aliasesByRelationshipPath().valueForKey(sourceKeyPath); destTableAlias = (String)expression.aliasesByRelationshipPath().valueForKey(baseKeyPath); if (null == destTableAlias) { - destTableAlias = "t" + (expression.aliasesByRelationshipPath().count()); // The first entry = "t0". + destTableAlias = EXISTS_ALIAS + (expression.aliasesByRelationshipPath().count()); // The first entry = "t0". expression.aliasesByRelationshipPath().takeValueForKey(destTableAlias, baseKeyPath); } } else { // The exists clause is applied to the base table. - destTableAlias = "t" + expression.aliasesByRelationshipPath().count(); // Probably "t1" + destTableAlias = EXISTS_ALIAS + expression.aliasesByRelationshipPath().count(); // Probably "t1" } - EOAttribute sourceKeyAttribute = srcEntity.primaryKeyAttributes().lastObject(); - String sourceKey = expression.sqlStringForAttribute(sourceKeyAttribute); - - EOAttribute destKeyAttribute = relationship.destinationAttributes().lastObject(); - String destKey = expression.sqlStringForAttribute(destKeyAttribute); + String srcEntityForeignKey = null; + NSArray sourceAttributes = relationship.sourceAttributes(); + if (sourceAttributes != null && sourceAttributes.count() > 0) { + EOAttribute fk = sourceAttributes.lastObject(); + srcEntityForeignKey = expression.sqlStringForAttribute(fk); + } else { + // (AR) could not find relationship from source object into "exists" clause, use primary key then instead + EOAttribute pk = srcEntity.primaryKeyAttributes().lastObject(); + srcEntityForeignKey = expression.sqlStringForAttribute(pk); + } + + EOJoin parentChildJoin = ERXArrayUtilities.firstObject(relationship.joins()); + String destEntityForeignKey = ".\"" + parentChildJoin.destinationAttribute().columnName() + "\""; EOQualifier qual = EOQualifierSQLGeneration.Support._schemaBasedQualifierWithRootEntity(subqualifier, destEntity); EOFetchSpecification fetchSpecification = new EOFetchSpecification(destEntity.name(), qual, null, false, true, null); @@ -211,7 +262,6 @@ public String sqlStringForSQLExpression(EOQualifier qualifier, EOSQLExpression e EOSQLExpressionFactory factory = context.database().adaptor().expressionFactory(); EOSQLExpression subExpression = factory.expressionForEntity(destEntity); - subExpression.aliasesByRelationshipPath().setObjectForKey(destTableAlias, ""); subExpression.setUseAliases(true); subExpression.prepareSelectExpressionWithAttributes(destEntity.primaryKeyAttributes(), false, fetchSpecification); @@ -219,13 +269,109 @@ public String sqlStringForSQLExpression(EOQualifier qualifier, EOSQLExpression e expression.addBindVariableDictionary((NSDictionary)bindEnumeration.nextElement()); } - StringBuilder sb = new StringBuilder(); - sb.append(" EXISTS ( "); - sb.append(ERXStringUtilities.replaceStringByStringInString("t0.", destTableAlias + ".", subExpression.statement())); - sb.append(" AND "); - sb.append(ERXStringUtilities.replaceStringByStringInString("t0.", destTableAlias + ".", destKey)); - sb.append(" = "); - sb.append(ERXStringUtilities.replaceStringByStringInString("t0.", sourceTableAlias + ".", sourceKey)); + String subExprStr = subExpression.statement(); + subExprStr = StringUtils.replace(subExprStr, "t0.", EXISTS_ALIAS + "0."); + subExprStr = StringUtils.replace(subExprStr, "t0 ", EXISTS_ALIAS + "0 "); + subExprStr = StringUtils.replace(subExprStr, "T0.", EXISTS_ALIAS + "0."); + subExprStr = StringUtils.replace(subExprStr, "T0 ", EXISTS_ALIAS + "0 "); + subExprStr = StringUtils.replace(subExprStr, "t1.", EXISTS_ALIAS + "1."); + subExprStr = StringUtils.replace(subExprStr, "t1 ", EXISTS_ALIAS + "1 "); + subExprStr = StringUtils.replace(subExprStr, "T1.", EXISTS_ALIAS + "1."); + subExprStr = StringUtils.replace(subExprStr, "T1 ", EXISTS_ALIAS + "1 "); + subExprStr = StringUtils.replace(subExprStr, "t2.", EXISTS_ALIAS + "2."); + subExprStr = StringUtils.replace(subExprStr, "t2 ", EXISTS_ALIAS + "2 "); + subExprStr = StringUtils.replace(subExprStr, "T2.", EXISTS_ALIAS + "2."); + subExprStr = StringUtils.replace(subExprStr, "T2 ", EXISTS_ALIAS + "2 "); + subExprStr = StringUtils.replace(subExprStr, "t3.", EXISTS_ALIAS + "3."); + subExprStr = StringUtils.replace(subExprStr, "t3 ", EXISTS_ALIAS + "3 "); + subExprStr = StringUtils.replace(subExprStr, "T3.", EXISTS_ALIAS + "3."); + subExprStr = StringUtils.replace(subExprStr, "T3 ", EXISTS_ALIAS + "3 "); + subExprStr = StringUtils.replace(subExprStr, "t4.", EXISTS_ALIAS + "4."); + subExprStr = StringUtils.replace(subExprStr, "t4 ", EXISTS_ALIAS + "4 "); + subExprStr = StringUtils.replace(subExprStr, "T4.", EXISTS_ALIAS + "4."); + subExprStr = StringUtils.replace(subExprStr, "T4 ", EXISTS_ALIAS + "4 "); + subExprStr = StringUtils.replace(subExprStr, "t5.", EXISTS_ALIAS + "5."); + subExprStr = StringUtils.replace(subExprStr, "T5.", EXISTS_ALIAS + "5."); + subExprStr = StringUtils.replace(subExprStr, "t5 ", EXISTS_ALIAS + "5 "); + subExprStr = StringUtils.replace(subExprStr, "T5 ", EXISTS_ALIAS + "5 "); + subExprStr = StringUtils.replace(subExprStr, "t6.", EXISTS_ALIAS + "6."); + subExprStr = StringUtils.replace(subExprStr, "t6 ", EXISTS_ALIAS + "6 "); + subExprStr = StringUtils.replace(subExprStr, "T6.", EXISTS_ALIAS + "6."); + subExprStr = StringUtils.replace(subExprStr, "T6 ", EXISTS_ALIAS + "6 "); + subExprStr = StringUtils.replace(subExprStr, "t7.", EXISTS_ALIAS + "7."); + subExprStr = StringUtils.replace(subExprStr, "t7 ", EXISTS_ALIAS + "7 "); + subExprStr = StringUtils.replace(subExprStr, "T7.", EXISTS_ALIAS + "7."); + subExprStr = StringUtils.replace(subExprStr, "T7 ", EXISTS_ALIAS + "7 "); + subExprStr = StringUtils.replace(subExprStr, "t8.", EXISTS_ALIAS + "8."); + subExprStr = StringUtils.replace(subExprStr, "t8 ", EXISTS_ALIAS + "8 "); + subExprStr = StringUtils.replace(subExprStr, "T8.", EXISTS_ALIAS + "8."); + subExprStr = StringUtils.replace(subExprStr, "T8 ", EXISTS_ALIAS + "8 "); + subExprStr = StringUtils.replace(subExprStr, "t9.", EXISTS_ALIAS + "9."); + subExprStr = StringUtils.replace(subExprStr, "t9 ", EXISTS_ALIAS + "9 "); + subExprStr = StringUtils.replace(subExprStr, "T9.", EXISTS_ALIAS + "9."); + subExprStr = StringUtils.replace(subExprStr, "T9 ", EXISTS_ALIAS + "9 "); + + // (AR) Note that the "space" character separates simple "t0 " from being part of a password hash or other + // valid data. It has never been 100% but generally true that you are replacing a table alias when we had + // a trailing space for match and replace. This fails when the "t0" is the last breath of subExprStr so + // let us match and replace at the end of the string now. + + if (subExprStr.endsWith("t0") || subExprStr.endsWith("T0")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "0"; + } else if (subExprStr.endsWith("t1") || subExprStr.endsWith("T1")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "1"; + } else if (subExprStr.endsWith("t2") || subExprStr.endsWith("T2")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "2"; + } else if (subExprStr.endsWith("t3") || subExprStr.endsWith("T3")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "3"; + } else if (subExprStr.endsWith("t4") || subExprStr.endsWith("T4")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "4"; + } else if (subExprStr.endsWith("t5") || subExprStr.endsWith("T5")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "5"; + } else if (subExprStr.endsWith("t6") || subExprStr.endsWith("T6")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "6"; + } else if (subExprStr.endsWith("t7") || subExprStr.endsWith("T7")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "7"; + } else if (subExprStr.endsWith("t8") || subExprStr.endsWith("T8")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "8"; + } else if (subExprStr.endsWith("t9") || subExprStr.endsWith("T9")) { + subExprStr = subExprStr.substring(0, subExprStr.length() - 2) + EXISTS_ALIAS + "9"; + } + + StringBuffer sb = new StringBuffer(); + if (existsQualifier.usesInQualInstead()) { + // (AR) Write the IN clause + sb.append(srcEntityForeignKey); + sb.append(" IN ( "); + + // (AR) Rewrite first SELECT part of subExprStr + EOAttribute destPK = destEntity.primaryKeyAttributes().lastObject(); + String destEntityPrimaryKey = expression.sqlStringForAttribute(destPK); + int indexOfFirstPeriod = destEntityPrimaryKey.indexOf("."); + destEntityPrimaryKey = destEntityPrimaryKey.substring(indexOfFirstPeriod); + subExprStr = StringUtils.replaceOnce( + subExprStr, + "SELECT " + EXISTS_ALIAS + "0" + destEntityPrimaryKey + " FROM", + "SELECT " + EXISTS_ALIAS + "0" + destEntityForeignKey + " FROM"); + } else { + sb.append(" EXISTS ( "); + } + sb.append(subExprStr); + if ( ! existsQualifier.usesInQualInstead()) { + String examineBuffer = sb.toString(); + examineBuffer = examineBuffer.substring(0, examineBuffer.length() - 1); + if (examineBuffer.endsWith(EXISTS_ALIAS)) { + // (AR) If we end with a table alias we must add a "where" clause + sb.append(" WHERE "); + } else { + // (AR) there was already a where clause so we must add a "and" + sb.append(" AND "); + } + + sb.append(EXISTS_ALIAS + "0" + destEntityForeignKey); + sb.append(" = "); + sb.append(StringUtils.replaceOnce(srcEntityForeignKey, "t0.", sourceTableAlias + ".")); + } sb.append(" ) "); return sb.toString(); } @@ -253,38 +399,39 @@ private String sqlStringForAttributeNamedInExpression(String name, EOSQLExpressi if (null == att) { return null; } return expression.sqlStringForAttribute(att); } - for (int i = 0; i < numPieces - 1; i++) { - rel = entity.anyRelationshipNamed(pieces.objectAtIndex(i)); - if (null == rel) { - return null; - } - path.addObject(rel); - entity = rel.destinationEntity(); + + for (int i = 0; i < numPieces - 1; i++) { + rel = entity.anyRelationshipNamed(pieces.objectAtIndex(i)); + if (null == rel) { + return null; } + path.addObject(rel); + entity = rel.destinationEntity(); + } - String key = pieces.lastObject(); - if (entity.anyRelationshipNamed(key) != null) { // Test first for a relationship. - rel = entity.anyRelationshipNamed(key); - if (rel.isFlattened()) { - String relPath = rel.relationshipPath(); - for (String relPart : NSArray.componentsSeparatedByString(relPath, ".")) { - rel = entity.anyRelationshipNamed(relPart); - path.addObject(rel); - entity = rel.destinationEntity(); - } - } else { + String key = pieces.lastObject(); + if (entity.anyRelationshipNamed(key) != null) { // Test first for a relationship. + rel = entity.anyRelationshipNamed(key); + if (rel.isFlattened()) { + String relPath = rel.relationshipPath(); + for (String relPart : NSArray.componentsSeparatedByString(relPath, ".")) { + rel = entity.anyRelationshipNamed(relPart); path.addObject(rel); + entity = rel.destinationEntity(); } - att = rel.destinationAttributes().lastObject(); - } else { // The test for an attribute. - att = entity.anyAttributeNamed(key); - } - - if (null == att) { - return null; + } else { + path.addObject(rel); } - path.addObject(att); + att = rel.destinationAttributes().lastObject(); + } else { // The test for an attribute. + att = entity.anyAttributeNamed(key); + } + if (null == att) { + return null; + } + path.addObject(att); + return expression.sqlStringForAttributePath(path); } @@ -328,7 +475,7 @@ public EOQualifier qualifierMigratedFromEntityRelationshipPath(EOQualifier quali */ @Override public Object clone() { - return new ERXExistsQualifier(subqualifier, baseKeyPath); + return new ERXExistsQualifier(subqualifier, baseKeyPath, usesInQualInstead()); } public Class classForCoder() { @@ -357,4 +504,43 @@ public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver unarchive (String)unarchiver.decodeObjectForKey("baseKeyPath")); } + @Override + public boolean evaluateWithObject(Object object) { + boolean match = false; + NSKeyValueCodingAdditions obj = (NSKeyValueCodingAdditions) object; + if (obj != null && subqualifier != null) { + NSKeyValueCodingAdditions finalObj = (NSKeyValueCodingAdditions) obj.valueForKeyPath(baseKeyPath); + if (finalObj != null) { + if (finalObj instanceof NSArray) { + NSArray objArray = (NSArray) finalObj; + objArray = ERXArrayUtilities.removeNullValues(objArray); + if (objArray != null && objArray.count() > 0) { + for (NSKeyValueCoding objInArray : objArray) { + try { + if (subqualifier.evaluateWithObject(objInArray)) { + match = true; + break; + } + } catch (UnknownKeyException unknownE) { + // ignore unknown keys because those objects wouldn't + // lead to a usable result. + } + } + } + } else { + match = subqualifier.evaluateWithObject(finalObj); + } + } + } + return match; + } + + public boolean usesInQualInstead() { + return usesInQualInstead; + } + + public void setUsesInQualInstead(boolean usesInQualInstead) { + this.usesInQualInstead = usesInQualInstead; + } + }