Skip to content

Commit

Permalink
Add optional MySQLExpression which makes use of mysql's binary keyword
Browse files Browse the repository at this point in the history
MySQL, by default, uses case insensitive comparisons. So "foo like
'bar'" is the same as "foo like 'BaR'". WO's case-insensitive like uses
the UPPER function which bypasses mysql indexing making it a slow
operation. The solution is to use "foo like 'bar'" for case insensitive
and "binary foo like 'Bar'" for case-sensitive. Similarly for sort
orderings, I believe, the binary keyword can be used for case sensitive
sort orderings.  Enable with property
com.webobjects.jdbcadaptor.MySQLExpression.enable=true
  • Loading branch information
ldeck authored and Pascal Robert committed Mar 16, 2012
1 parent dc2de9b commit 9995ba2
Show file tree
Hide file tree
Showing 2 changed files with 345 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
/**
*
*/
package com.webobjects.jdbcadaptor;

import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.webobjects.eoaccess.EOAttribute;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eocontrol.EOKeyComparisonQualifier;
import com.webobjects.eocontrol.EOKeyValueQualifier;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOQualifierVariable;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSSelector;
import com.webobjects.foundation._NSStringUtilities;

/**
* <p>
* Overrides the default EOF MySQLExpression to adjust the sql generation of
* string comparisons.
* </p>
* <p>
* MySQL's default behaviour, as per usual, is to ignore the standards. Thus an
* sql <code>like</code> performs a case insensitive comparison rather than case
* sensitive which is vexing.
* </p>
* <p>
* To enforce standard behaviour you can tell mysql by using
* <code>like binary</code> for a case sensitive comparison. Thus a case
* insensitive comparison simply requires using a <code>like</code> in order to
* obtain the results desired.
* </p>
* <p>
* <b>Note</b>: This assumes that you're not using binary columns.
* </p>
* <p>
* The alternative is to define all of your columns as binary columns and
* specifically specify the collation to use for case insensitive comparisons.
* But that's a more complex approach in the author's view.
* </p>
* <p>
* Another approach is to only every use qualifiers and sort orderings that are
* case sensitive (semantically) and choose in your model what external type to
* map to in order to control the behaviour. In my view this is bad practice
* because you're separating the logic of queries and returning results that are
* not intended according to the code.
* </p>
* <p>
* To have this class enabled as the runtime MySQLExpression define the
* following property.
* </p>
* <p>
* com.webobjects.jdbcadaptor.MySQLExpression.enable=true
* </p>
* <p>
* To summarise:
* </p>
* <ul>
* <li>LIKE UPPER(foo) becomes LIKE foo
* <li>LIKE foo becomes LIKE BINARY foo
* </ul>
*
* @author ldeck
*/
public class MySQLExpression
extends
com.webobjects.jdbcadaptor.MySQLPlugIn.MySQLExpression
{

private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger( MySQLExpression.class );
private static final List< NSSelector > SORT_ORDERING_ASC_SELECTORS =
Arrays.asList( EOSortOrdering.CompareAscending, EOSortOrdering.CompareCaseInsensitiveAscending );
private static final List< NSSelector > SORT_ORDERING_BIN_SELECTORS = Arrays.asList( EOSortOrdering.CompareAscending, EOSortOrdering.CompareDescending );
private static final List< NSSelector > SORT_ORDERING_DESC_SELECTORS =
Arrays.asList( EOSortOrdering.CompareCaseInsensitiveDescending, EOSortOrdering.CompareDescending );

private final Pattern likeOperatorRegex;
private final Pattern upperFunctionNameRegex;

/**
* @param entity
*/
public MySQLExpression( EOEntity entity )
{
super( entity );
this.upperFunctionNameRegex = Pattern.compile( "\\Q" + this._upperFunctionName + "\\E\\(([^\\)]+)\\)" );
this.likeOperatorRegex = Pattern.compile( "([Ll][Ii][Kk][Ee])" );
}

/**
* @see com.webobjects.eoaccess.EOSQLExpression#addOrderByAttributeOrdering(com.webobjects.eocontrol.EOSortOrdering)
*/
@Override
public void addOrderByAttributeOrdering( EOSortOrdering sortOrdering )
{
NSSelector< ? > selector = sortOrdering.selector();
String attPath = sortOrdering.key();
String sqlString1 = sqlStringForAttributeNamed( attPath );
if ( sqlString1 == null )
{
throw new IllegalStateException( new StringBuilder()
.append( "addOrderByAttributeOrdering: attempt to generate SQL for " )
.append( sortOrdering.getClass().getName() )
.append( " " )
.append( sortOrdering )
.append( " failed because attribute identified by key '" )
.append( sortOrdering.key() )
.append( "' was not reachable from from entity '" )
.append( this._entity.name() )
.append( "'" )
.toString() );
}
String format;
boolean isAscending = false;
if ( ( isAscending = SORT_ORDERING_ASC_SELECTORS.contains( selector ) ) || SORT_ORDERING_DESC_SELECTORS.contains( selector ) )
{
String orderType = isAscending ? "ASC" : "DESC";
String binaryOperator =
SORT_ORDERING_BIN_SELECTORS.contains( selector )
&& entity()._attributeForPath( attPath ).adaptorValueType() == EOAttribute.AdaptorCharactersType ? "BINARY " : "";

format = _NSStringUtilities.concat( binaryOperator, " ", sqlString1, " ", orderType );
}
else
{
format = _NSStringUtilities.concat( "(", sqlString1, ")" );
}
appendItemToListString( format, _orderByString() );
}

/**
* @see com.webobjects.jdbcadaptor.JDBCExpression#appendItemToOrderByString(java.lang.String)
*/
@Override
protected void appendItemToOrderByString( String sqlString )
{
super.appendItemToOrderByString( replaceStringForCaseInsensitiveLike( sqlString ) );
}

protected Pattern likeOperatorRegex()
{
return this.likeOperatorRegex;
}

protected String replaceStringForCaseInsensitiveLike( String string )
{
StringBuffer result = new StringBuffer();

Matcher matcher = upperFunctionNameRegex().matcher( string );
while ( matcher.find() )
{
matcher.appendReplacement( result, Matcher.quoteReplacement( matcher.group( 1 ) ) );
}
matcher.appendTail( result );

return result.toString();
}

protected String replaceStringForCaseSensitiveLike( String string )
{
StringBuffer result = new StringBuffer();

Matcher matcher = likeOperatorRegex().matcher( string );
while ( matcher.find() )
{
matcher.appendReplacement( result, Matcher.quoteReplacement( matcher.group( 1 ) + " BINARY" ) );
}
matcher.appendTail( result );

return result.toString();
}

/**
* @see com.webobjects.eoaccess.EOSQLExpression#sqlStringForCaseInsensitiveLike(java.lang.String,
* java.lang.String)
*/
@Override
public String sqlStringForCaseInsensitiveLike( String valueString, String keyString )
{
return replaceStringForCaseInsensitiveLike( super.sqlStringForCaseInsensitiveLike( valueString, keyString ) );
}

/**
* @see com.webobjects.eoaccess.EOSQLExpression#sqlStringForKeyComparisonQualifier(com.webobjects.eocontrol.EOKeyComparisonQualifier)
*/
@Override
public String sqlStringForKeyComparisonQualifier( EOKeyComparisonQualifier qualifier )
{
String leftKey = qualifier.leftKey();
String rightKey = qualifier.rightKey();
if ( leftKey != null && leftKey.equals( rightKey ) )
{
return "(1=1)";
}
EOAttribute att = this._entity._attributeForPath( leftKey );
String leftKeyString = sqlStringForAttributeNamed( leftKey );
if ( leftKeyString == null )
{
throw new IllegalStateException( new StringBuilder()
.append( "sqlStringForKeyComparisonQualifier: attempt to generate SQL for " )
.append( qualifier.getClass().getName() )
.append( " " )
.append( qualifier )
.append( " failed because attribute identified by key '" )
.append( leftKey )
.append( "' was not reachable from from entity '" )
.append( this._entity.name() )
.append( "'" )
.toString() );
}
leftKeyString = formatSQLString( leftKeyString, att.readFormat() );
att = this._entity._attributeForPath( rightKey );
String rightKeyString = sqlStringForAttributeNamed( rightKey );
if ( rightKeyString == null )
{
throw new IllegalStateException( new StringBuilder()
.append( "sqlStringForKeyComparisonQualifier: attempt to generate SQL for " )
.append( qualifier.getClass().getName() )
.append( " " )
.append( qualifier )
.append( " failed because attribute identified by key '" )
.append( rightKey )
.append( "' was not reachable from from entity '" )
.append( this._entity.name() )
.append( "'" )
.toString() );
}
else
{
rightKeyString = formatSQLString( rightKeyString, att.readFormat() );
String operatorString = sqlStringForSelector( qualifier.selector(), null );

EOAttribute leftAttribute = this._entity._attributeForPath( leftKey );
EOAttribute rightAttribute = this._entity._attributeForPath( rightKey );

NSSelector qualifierSelector = qualifier.selector();
boolean isLike =
qualifierSelector.equals( EOQualifier.QualifierOperatorLike ) || qualifierSelector.equals( EOQualifier.QualifierOperatorCaseInsensitiveLike );
boolean isStringComparison =
isLike
|| EOAttribute.AdaptorCharactersType == leftAttribute.adaptorValueType()
|| EOAttribute.AdaptorCharactersType == rightAttribute.adaptorValueType();

String binaryOperator = isStringComparison ? "BINARY " : "";

return _NSStringUtilities.concat( binaryOperator, leftKeyString, " ", operatorString, " ", rightKeyString );
}
}

/**
* @see com.webobjects.eoaccess.EOSQLExpression#sqlStringForKeyValueQualifier(com.webobjects.eocontrol.EOKeyValueQualifier)
*/
@Override
public String sqlStringForKeyValueQualifier( EOKeyValueQualifier qualifier )
{
String key = qualifier.key();
String keyString = sqlStringForAttributeNamed( key );
if ( keyString == null )
{
throw new IllegalStateException( new StringBuilder()
.append( "sqlStringForKeyValueQualifier: attempt to generate SQL for " )
.append( qualifier.getClass().getName() )
.append( " " )
.append( qualifier )
.append( " failed because attribute identified by key '" )
.append( key )
.append( "' was not reachable from from entity '" )
.append( this._entity.name() )
.append( "'" )
.toString() );
}
Object qualifierValue = qualifier.value();
if ( qualifierValue instanceof EOQualifierVariable )
{
throw new IllegalStateException( new StringBuilder()
.append( "sqlStringForKeyValueQualifier: attempt to generate SQL for " )
.append( qualifier.getClass().getName() )
.append( " " )
.append( qualifier )
.append( " failed because the qualifier variable '$" )
.append( ( ( EOQualifierVariable )qualifierValue ).key() )
.append( "' is unbound." )
.toString() );
}

EOAttribute attribute = this._entity._attributeForPath( key );

keyString = formatSQLString( keyString, attribute.readFormat() );
NSSelector qualifierSelector = qualifier.selector();
boolean isLike =
qualifierSelector.equals( EOQualifier.QualifierOperatorLike ) || qualifierSelector.equals( EOQualifier.QualifierOperatorCaseInsensitiveLike );
boolean isStringComparison = isLike || EOAttribute.AdaptorCharactersType == attribute.adaptorValueType();

Object value;
if ( isLike )
{
value = sqlPatternFromShellPattern( ( String )qualifierValue );
}
else
{
value = qualifierValue;
}
String string;
if ( qualifierSelector.equals( EOQualifier.QualifierOperatorCaseInsensitiveLike ) )
{
String valueString = sqlStringForValue( value, key );
String operatorString = sqlStringForSelector( qualifierSelector, value );
string = sqlStringForCaseInsensitiveLike( valueString, keyString );
}
else if ( EOQualifier.QualifierOperatorLike.equals( qualifierSelector ) || isStringComparison )
{
String valueString = sqlStringForValue( value, key );
String operatorString = sqlStringForSelector( qualifierSelector, value );
string = _NSStringUtilities.concat( "BINARY ", keyString, " ", operatorString, " ", valueString );
}
else
{
String valueString = sqlStringForValue( value, key );
String operatorString = sqlStringForSelector( qualifierSelector, value );
string = _NSStringUtilities.concat( keyString, " ", operatorString, " ", valueString );
}
if ( isLike )
{
char escapeChar = sqlEscapeChar();
if ( escapeChar != 0 )
{
string = _NSStringUtilities.concat( string, new StringBuilder().append( " ESCAPE '" ).append( escapeChar ).append( "'" ).toString() );
}
}
return string;
}

protected Pattern upperFunctionNameRegex()
{
return this.upperFunctionNameRegex;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.webobjects.foundation.NSLog;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSProperties;
import com.webobjects.foundation.NSPropertyListSerialization;
import com.webobjects.foundation._NSStringUtilities;

Expand Down Expand Up @@ -312,6 +313,9 @@ public String databaseProductName() {

@Override
public Class<com.webobjects.jdbcadaptor._MySQLPlugIn.MySQLExpression> defaultExpressionClass() {
if (NSProperties.booleanForKeyWithDefault("com.webobjects.jdbcadaptor.MySQLExpression.enable", false)) {
return com.webobjects.jdbcadaptor.MySQLExpression.class;
}
return com.webobjects.jdbcadaptor._MySQLPlugIn.MySQLExpression.class;
}

Expand Down

0 comments on commit 9995ba2

Please sign in to comment.