HHH-17435 Allow input param as trim character and fix single quote

This commit is contained in:
Marco Belladelli 2024-01-04 11:55:23 +01:00 committed by Christian Beikov
parent 2b025e9b4e
commit 2fce965730
19 changed files with 188 additions and 95 deletions

View File

@ -138,23 +138,23 @@ public class AltibaseDialect extends Dialect {
}
@Override
public String trimPattern(TrimSpec specification, char character) {
public String trimPattern(TrimSpec specification, boolean isWhitespace) {
switch ( specification ) {
case BOTH:
return character == ' '
return isWhitespace
? "trim(?1)"
: "trim(?1, '" + character + "')";
: "trim(?1,?2)";
case LEADING:
return character == ' '
return isWhitespace
? "ltrim(?1)"
: "ltrim(?1,'" + character + "')";
: "ltrim(?1,?2)";
case TRAILING:
return character == ' '
return isWhitespace
? "rtrim(?1)"
: "rtrim(?1,'" + character + "')";
: "rtrim(?1,?2)";
}
return super.trimPattern( specification, character );
return super.trimPattern( specification, isWhitespace );
}
@Override

View File

@ -34,6 +34,7 @@ import org.hibernate.dialect.function.CountFunction;
import org.hibernate.dialect.function.DB2FormatEmulation;
import org.hibernate.dialect.function.DB2PositionFunction;
import org.hibernate.dialect.function.DB2SubstringFunction;
import org.hibernate.dialect.function.TrimFunction;
import org.hibernate.dialect.identity.DB2IdentityColumnSupport;
import org.hibernate.dialect.identity.IdentityColumnSupport;
import org.hibernate.dialect.pagination.DB2LimitHandler;
@ -403,6 +404,13 @@ public class DB2LegacyDialect extends Dialect {
.setArgumentListSignature("(STRING string, STRING pattern)")
.register();
//trim() requires trim characters to be constant literals
functionContributions.getFunctionRegistry().register( "trim", new TrimFunction(
this,
functionContributions.getTypeConfiguration(),
SqlAstNodeRenderingMode.INLINE_PARAMETERS
) );
functionFactory.windowFunctions();
if ( getDB2Version().isSameOrAfter( 9, 5 ) ) {
functionFactory.listagg( null );

View File

@ -16,6 +16,7 @@ import org.hibernate.StaleObjectStateException;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.dialect.*;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.function.TrimFunction;
import org.hibernate.dialect.identity.HSQLIdentityColumnSupport;
import org.hibernate.dialect.identity.IdentityColumnSupport;
import org.hibernate.dialect.lock.LockingStrategy;
@ -268,6 +269,13 @@ public class HSQLLegacyDialect extends Dialect {
functionFactory.arrayTrim_trim_array();
functionFactory.arrayFill_hsql();
functionFactory.arrayToString_hsql();
//trim() requires parameters to be cast when used as trim character
functionContributions.getFunctionRegistry().register( "trim", new TrimFunction(
this,
functionContributions.getTypeConfiguration(),
SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER
) );
}
@Override

View File

@ -203,8 +203,8 @@ public class MaxDBDialect extends Dialect {
}
@Override
public String trimPattern(TrimSpec specification, char character) {
return AbstractTransactSQLDialect.replaceLtrimRtrim( specification, character);
public String trimPattern(TrimSpec specification, boolean isWhitespace) {
return AbstractTransactSQLDialect.replaceLtrimRtrim( specification, isWhitespace );
}
@Override

View File

@ -431,7 +431,7 @@ public class RDMSOS2200Dialect extends Dialect {
}
@Override
public String trimPattern(TrimSpec specification, char character) {
return AbstractTransactSQLDialect.replaceLtrimRtrim( specification, character);
public String trimPattern(TrimSpec specification, boolean isWhitespace) {
return AbstractTransactSQLDialect.replaceLtrimRtrim( specification, isWhitespace );
}
}

View File

@ -390,25 +390,25 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect {
}
@Override
public String trimPattern(TrimSpec specification, char character) {
public String trimPattern(TrimSpec specification, boolean isWhitespace) {
if ( getVersion().isSameOrAfter( 16 ) ) {
switch ( specification ) {
case BOTH:
return character == ' '
return isWhitespace
? "trim(?1)"
: "trim('" + character + "' from ?1)";
: "trim(?2 from ?1)";
case LEADING:
return character == ' '
return isWhitespace
? "ltrim(?1)"
: "ltrim(?1,'" + character + "')";
: "ltrim(?1,?2)";
case TRAILING:
return character == ' '
return isWhitespace
? "rtrim(?1)"
: "rtrim(?1,'" + character + "')";
: "rtrim(?1,?2)";
}
throw new UnsupportedOperationException( "Unsupported specification: " + specification );
}
return super.trimPattern( specification, character );
return super.trimPattern( specification, isWhitespace );
}
@Override

View File

@ -353,20 +353,20 @@ public class SQLiteDialect extends Dialect {
}
@Override
public String trimPattern(TrimSpec specification, char character) {
public String trimPattern(TrimSpec specification, boolean isWhitespace) {
switch ( specification ) {
case BOTH:
return character == ' '
return isWhitespace
? "trim(?1)"
: "trim(?1,'" + character + "')";
: "trim(?1,?2)";
case LEADING:
return character == ' '
return isWhitespace
? "ltrim(?1)"
: "ltrim(?1,'" + character + "')";
: "ltrim(?1,?2)";
case TRAILING:
return character == ' '
return isWhitespace
? "rtrim(?1)"
: "rtrim(?1,'" + character + "')";
: "rtrim(?1,?2)";
}
throw new UnsupportedOperationException( "Unsupported specification: " + specification );
}

View File

@ -425,12 +425,6 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect {
return "datediff(?1,?2,?3)";
}
@Override
public String trimPattern(TrimSpec specification, char character) {
return super.trimPattern(specification, character)
.replace("replace", "str_replace");
}
@Override
public void appendDatetimeFormat(SqlAppender appender, String format) {
throw new UnsupportedOperationException( "format() function not supported on Sybase");

View File

@ -1369,6 +1369,7 @@ trimSpecification
trimCharacter
: STRING_LITERAL
| parameter
;
/**

View File

@ -166,28 +166,32 @@ public abstract class AbstractTransactSQLDialect extends Dialect {
}
@Override
public String trimPattern(TrimSpec specification, char character) {
return replaceLtrimRtrim(specification, character);
public String trimPattern(TrimSpec specification, boolean isWhitespace) {
return replaceLtrimRtrim( specification, isWhitespace );
}
/**
* @deprecated Use {@link #replaceLtrimRtrim(TrimSpec, boolean)} instead.
*/
@Deprecated( forRemoval = true )
public static String replaceLtrimRtrim(TrimSpec specification, char character) {
boolean blank = character == ' ';
return replaceLtrimRtrim( specification, character == ' ' );
}
public static String replaceLtrimRtrim(TrimSpec specification, boolean isWhitespace) {
switch ( specification ) {
case LEADING:
return blank
return isWhitespace
? "ltrim(?1)"
: "replace(replace(ltrim(replace(replace(?1,' ','#%#%'),'@',' ')),' ','@'),'#%#%',' ')"
.replace('@', character);
: "substring(?1,patindex('%[^'+?2+']%',?1),len(?1)-patindex('%[^'+?2+']%',?1)+1)";
case TRAILING:
return blank
return isWhitespace
? "rtrim(?1)"
: "replace(replace(rtrim(replace(replace(?1,' ','#%#%'),'@',' ')),' ','@'),'#%#%',' ')"
.replace('@', character);
: "substring(?1,1,len(?1)-patindex('%[^'+?2+']%',reverse(?1))+1)";
default:
return blank
return isWhitespace
? "ltrim(rtrim(?1))"
: "replace(replace(ltrim(rtrim(replace(replace(?1,' ','#%#%'),'@',' '))),' ','@'),'#%#%',' ')"
.replace('@', character);
: "substring(?1,patindex('%[^'+?2+']%',?1),len(?1)-patindex('%[^'+?2+']%',?1)-patindex('%[^'+?2+']%',reverse(?1))+2)";
}
}

View File

@ -28,6 +28,7 @@ import org.hibernate.dialect.function.CountFunction;
import org.hibernate.dialect.function.DB2FormatEmulation;
import org.hibernate.dialect.function.DB2PositionFunction;
import org.hibernate.dialect.function.DB2SubstringFunction;
import org.hibernate.dialect.function.TrimFunction;
import org.hibernate.dialect.identity.DB2IdentityColumnSupport;
import org.hibernate.dialect.identity.IdentityColumnSupport;
import org.hibernate.dialect.pagination.DB2LimitHandler;
@ -391,6 +392,13 @@ public class DB2Dialect extends Dialect {
.setArgumentListSignature("(STRING string, STRING pattern)")
.register();
//trim() requires trim characters to be constant literals
functionContributions.getFunctionRegistry().register( "trim", new TrimFunction(
this,
functionContributions.getTypeConfiguration(),
SqlAstNodeRenderingMode.INLINE_PARAMETERS
) );
functionFactory.windowFunctions();
functionFactory.listagg( null );
}

View File

@ -1484,11 +1484,29 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun
*
* @param specification {@code leading} or {@code trailing}
* @param character the character to trim
*
* @deprecated Use {@link #trimPattern(TrimSpec, boolean)} instead.
*/
@Deprecated( forRemoval = true )
public String trimPattern(TrimSpec specification, char character) {
return character == ' '
? "trim(" + specification + " from ?1)"
: "trim(" + specification + " '" + character + "' from ?1)";
return trimPattern( specification, character == ' ' );
}
/**
* Obtain a pattern for the SQL equivalent to a
* {@code trim()} function call. The resulting
* pattern must contain a ?1 placeholder for the
* argument of type {@link String} and a ?2 placeholder
* for the trim character if {@code isWhitespace}
* was false.
*
* @param specification {@linkplain TrimSpec#LEADING leading}, {@linkplain TrimSpec#TRAILING trailing}
* or {@linkplain TrimSpec#BOTH both}
* @param isWhitespace {@code true} if the trim character is a whitespace and can be omitted,
* {@code false} if it must be explicit and a ?2 placeholder should be included in the pattern
*/
public String trimPattern(TrimSpec specification, boolean isWhitespace) {
return "trim(" + specification + ( isWhitespace ? "" : " ?2" ) + " from ?1)";
}
/**

View File

@ -290,6 +290,11 @@ public class DialectDelegateWrapper extends Dialect {
return wrapped.trimPattern( specification, character );
}
@Override
public String trimPattern(TrimSpec specification, boolean isWhitespace) {
return wrapped.trimPattern( specification, isWhitespace );
}
@Override
public boolean supportsFractionalTimestampArithmetic() {
return wrapped.supportsFractionalTimestampArithmetic();

View File

@ -12,6 +12,7 @@ import java.sql.Types;
import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.dialect.function.CommonFunctionFactory;
import org.hibernate.dialect.function.TrimFunction;
import org.hibernate.dialect.identity.HSQLIdentityColumnSupport;
import org.hibernate.dialect.identity.IdentityColumnSupport;
import org.hibernate.dialect.pagination.LimitHandler;
@ -208,6 +209,13 @@ public class HSQLDialect extends Dialect {
functionFactory.arrayTrim_trim_array();
functionFactory.arrayFill_hsql();
functionFactory.arrayToString_hsql();
//trim() requires parameters to be cast when used as trim character
functionContributions.getFunctionRegistry().register( "trim", new TrimFunction(
this,
functionContributions.getTypeConfiguration(),
SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER
) );
}
@Override

View File

@ -399,25 +399,25 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
}
@Override
public String trimPattern(TrimSpec specification, char character) {
public String trimPattern(TrimSpec specification, boolean isWhitespace) {
if ( getVersion().isSameOrAfter( 16 ) ) {
switch ( specification ) {
case BOTH:
return character == ' '
return isWhitespace
? "trim(?1)"
: "trim('" + character + "' from ?1)";
: "trim(?2 from ?1)";
case LEADING:
return character == ' '
return isWhitespace
? "ltrim(?1)"
: "ltrim(?1,'" + character + "')";
: "ltrim(?1,?2)";
case TRAILING:
return character == ' '
return isWhitespace
? "rtrim(?1)"
: "rtrim(?1,'" + character + "')";
: "rtrim(?1,?2)";
}
throw new UnsupportedOperationException( "Unsupported specification: " + specification );
}
return super.trimPattern( specification, character );
return super.trimPattern( specification, isWhitespace );
}
@Override

View File

@ -447,12 +447,6 @@ public class SybaseDialect extends AbstractTransactSQLDialect {
return "datediff(?1,?2,?3)";
}
@Override
public String trimPattern(TrimSpec specification, char character) {
return super.trimPattern(specification, character)
.replace("replace", "str_replace");
}
@Override
public void appendDatetimeFormat(SqlAppender appender, String format) {
throw new UnsupportedOperationException( "format() function not supported on Sybase");

View File

@ -6,44 +6,56 @@
*/
package org.hibernate.dialect.function;
import java.util.Collections;
import java.util.List;
import org.hibernate.dialect.Dialect;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.sqm.TrimSpec;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.FunctionArgumentException;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.query.sqm.produce.function.internal.PatternRenderer;
import org.hibernate.query.sqm.sql.internal.SqmParameterInterpretation;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.Literal;
import org.hibernate.sql.ast.tree.expression.TrimSpecification;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.spi.TypeConfiguration;
import java.util.Collections;
import java.util.List;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TRIM_SPEC;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TRIM_SPEC;
/**
* ANSI SQL-standard {@code trim()} function, which has a funny syntax
* involving a {@link TrimSpec}, and portability is achieved using
* {@link Dialect#trimPattern(TrimSpec, char)}.
* {@link Dialect#trimPattern(TrimSpec, boolean)}.
* <p>
* For example, {@code trim(leading ' ' from text)}.
*
* @author Gavin King
*/
public class TrimFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
private final Dialect dialect;
private SqlAstNodeRenderingMode argumentRenderingMode;
public TrimFunction(Dialect dialect, TypeConfiguration typeConfiguration) {
this( dialect, typeConfiguration, SqlAstNodeRenderingMode.DEFAULT );
}
public TrimFunction(
Dialect dialect,
TypeConfiguration typeConfiguration,
SqlAstNodeRenderingMode argumentRenderingMode) {
super(
"trim",
new ArgumentTypesValidator(
@ -56,6 +68,7 @@ public class TrimFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, TRIM_SPEC, STRING, STRING )
);
this.dialect = dialect;
this.argumentRenderingMode = argumentRenderingMode;
}
@Override
@ -65,12 +78,36 @@ public class TrimFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
final TrimSpec specification = ( (TrimSpecification) sqlAstArguments.get( 0 ) ).getSpecification();
final Object trimCharacter = ( (Literal) sqlAstArguments.get( 1 ) ).getLiteralValue();
final SqlAstNode trimCharacter = sqlAstArguments.get( 1 );
final boolean isWhitespace = isWhitespace( trimCharacter );
final Expression sourceExpr = (Expression) sqlAstArguments.get( 2 );
String trim = dialect.trimPattern( specification, (char) trimCharacter );
final String trim = dialect.trimPattern( specification, isWhitespace );
new PatternRenderer( trim ).render( sqlAppender, Collections.singletonList( sourceExpr ), walker );
final List<? extends SqlAstNode> args = isWhitespace ?
Collections.singletonList( sourceExpr ) :
List.of( sourceExpr, trimCharacter );
new PatternRenderer( trim, argumentRenderingMode ).render( sqlAppender, args, walker );
}
private static boolean isWhitespace(SqlAstNode trimCharacter) {
if ( trimCharacter instanceof Literal ) {
final char literalValue = (char) ( (Literal) trimCharacter ).getLiteralValue();
return literalValue == ' ';
}
else {
assert trimCharacter instanceof SqmParameterInterpretation;
final JdbcType jdbcType = ( (SqmParameterInterpretation) trimCharacter ).getExpressionType()
.getSingleJdbcMapping()
.getJdbcType();
if ( jdbcType.getJdbcTypeCode() != SqlTypes.CHAR ) {
throw new FunctionArgumentException( String.format(
"Expected parameter used as trim character to be Character typed, instead was [%s]",
jdbcType.getFriendlyName()
) );
}
return false;
}
}
// @Override

View File

@ -4782,7 +4782,7 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
public SqmExpression<?> visitTrimFunction(HqlParser.TrimFunctionContext ctx) {
final SqmExpression<?> source = (SqmExpression<?>) ctx.expression().accept( this );
final SqmTrimSpecification trimSpec = visitTrimSpecification( ctx.trimSpecification() );;
final SqmLiteral<Character> trimChar = visitTrimCharacter( ctx.trimCharacter() );
final SqmExpression<Character> trimChar = visitTrimCharacter( ctx.trimCharacter() );
return getFunctionDescriptor("trim").generateSqmExpression(
asList(
@ -4816,15 +4816,25 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
}
@Override
public SqmLiteral<Character> visitTrimCharacter(HqlParser.TrimCharacterContext ctx) {
final String trimCharText = ctx != null
? unquoteStringLiteral( ctx.getText() )
: " "; // JPA says space is the default
if ( trimCharText.length() != 1 ) {
throw new SemanticException( "Trim character for trim() function must be single character, found '" + trimCharText + "'" );
public SqmExpression<Character> visitTrimCharacter(HqlParser.TrimCharacterContext ctx) {
final String trimCharText;
if ( ctx == null ) {
// JPA says space is the default
trimCharText = " ";
}
else {
final ParseTree child = ctx.getChild( 0 );
if ( child instanceof HqlParser.ParameterContext ) {
//noinspection unchecked
return (SqmExpression<Character>) child.accept( this );
}
else {
trimCharText = unquoteStringLiteral( ctx.getText() );
if ( trimCharText.length() != 1 ) {
throw new SemanticException( "Trim character for trim() function must be single character, found '" + trimCharText + "'" );
}
}
}
return new SqmLiteral<>(
trimCharText.charAt( 0 ),
resolveExpressibleTypeBasic( Character.class ),

View File

@ -32,6 +32,7 @@ import org.hibernate.type.descriptor.jdbc.CharJdbcType;
import org.hibernate.type.internal.BasicTypeImpl;
import org.hibernate.type.spi.TypeConfiguration;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.ServiceRegistryScope;
import org.junit.jupiter.api.Test;
@ -41,65 +42,62 @@ import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* TODO : javadoc
* Tests correct rendering of trim function emulation for {@link org.hibernate.dialect.AbstractTransactSQLDialect} dialects.
*
* @author Christian Beikov
*/
@ServiceRegistry
public class AnsiTrimEmulationFunctionTest {
private static final String trimSource = "a.column";
private static final String LEADING = "substring(?1,patindex('%[^'+?2+']%',?1),len(?1)-patindex('%[^'+?2+']%',?1)+1)";
private static final String TRAILING = "substring(?1,1,len(?1)-patindex('%[^'+?2+']%',reverse(?1))+1)";
private static final String BOTH = "substring(?1,patindex('%[^'+?2+']%',?1),len(?1)-patindex('%[^'+?2+']%',?1)-patindex('%[^'+?2+']%',reverse(?1))+2)";
@Test
@RequiresDialect( SQLServerDialect.class )
public void testBasicSqlServerProcessing(ServiceRegistryScope scope) {
Dialect dialect = new SQLServerDialect();
TrimFunction function = new TrimFunction( dialect, new TypeConfiguration() );
performBasicSpaceTrimmingTests( dialect, scope.getRegistry(), function );
final String expectedTrimPrep = "replace(replace(a.column,' ','#%#%'),'-',' ')";
final String expectedPostTrimPrefix = "replace(replace(";
final String expectedPostTrimSuffix = ",' ','-'),'#%#%',' ')";
// -> trim(LEADING '-' FROM a.column)
String rendered = render( dialect, scope.getRegistry(), function, TrimSpec.LEADING, '-', trimSource );
String expected = expectedPostTrimPrefix + "ltrim(" + expectedTrimPrep + ")" + expectedPostTrimSuffix;
String expected = LEADING.replace( "?1", trimSource ).replace( "?2", "'-'" );
assertEquals( expected, rendered );
// -> trim(TRAILING '-' FROM a.column)
rendered = render( dialect, scope.getRegistry(), function, TrimSpec.TRAILING, '-', trimSource );
expected = expectedPostTrimPrefix + "rtrim(" + expectedTrimPrep + ")" + expectedPostTrimSuffix;
expected = TRAILING.replace( "?1", trimSource ).replace( "?2", "'-'" );
assertEquals( expected, rendered );
// -> trim(BOTH '-' FROM a.column)
rendered = render( dialect, scope.getRegistry(), function, TrimSpec.BOTH, '-', trimSource );
expected = expectedPostTrimPrefix + "ltrim(rtrim(" + expectedTrimPrep + "))" + expectedPostTrimSuffix;
expected = BOTH.replace( "?1", trimSource ).replace( "?2", "'-'" );
assertEquals( expected, rendered );
}
@Test
@RequiresDialect( SybaseDialect.class )
public void testBasicSybaseProcessing(ServiceRegistryScope scope) {
Dialect dialect = new SybaseDialect();
TrimFunction function = new TrimFunction( dialect, new TypeConfiguration() );
performBasicSpaceTrimmingTests( dialect, scope.getRegistry(), function );
final String expectedTrimPrep = "str_replace(str_replace(a.column,' ','#%#%'),'-',' ')";
final String expectedPostTrimPrefix = "str_replace(str_replace(";
final String expectedPostTrimSuffix = ",' ','-'),'#%#%',' ')";
// -> trim(LEADING '-' FROM a.column)
String rendered = render( dialect, scope.getRegistry(), function, TrimSpec.LEADING, '-', trimSource );
String expected = expectedPostTrimPrefix + "ltrim(" + expectedTrimPrep + ")" + expectedPostTrimSuffix;
String expected = LEADING.replace( "?1", trimSource ).replace( "?2", "'-'" );
assertEquals( expected, rendered );
// -> trim(TRAILING '-' FROM a.column)
rendered = render( dialect, scope.getRegistry(), function, TrimSpec.TRAILING, '-', trimSource );
expected = expectedPostTrimPrefix + "rtrim(" + expectedTrimPrep + ")" + expectedPostTrimSuffix;
expected = TRAILING.replace( "?1", trimSource ).replace( "?2", "'-'" );
assertEquals( expected, rendered );
// -> trim(BOTH '-' FROM a.column)
rendered = render( dialect, scope.getRegistry(), function, TrimSpec.BOTH, '-', trimSource );
expected = expectedPostTrimPrefix + "ltrim(rtrim(" + expectedTrimPrep + "))" + expectedPostTrimSuffix;
expected = BOTH.replace( "?1", trimSource ).replace( "?2", "'-'" );
assertEquals( expected, rendered );
}