Add validation for format function and remove `aa` format as no database supports long/short AM/PM markers

This commit is contained in:
Christian Beikov 2022-01-06 18:24:12 +01:00
parent b8fab567cc
commit c284315931
14 changed files with 42 additions and 55 deletions

View File

@ -517,7 +517,6 @@ public class InformixDialect extends Dialect {
.replace("d", "%e")
//am pm
.replace("aa", "%p") //?????
.replace("a", "%p") //?????
//hour

View File

@ -593,7 +593,6 @@ public class SQLiteDialect extends Dialect {
.replace("d", "%d") //?????
//am pm
.replace("aa", "%p") //?????
.replace("a", "%p") //?????
//hour

View File

@ -231,7 +231,7 @@ public class CockroachDialect extends Dialect {
queryEngine.getSqmFunctionRegistry().namedDescriptorBuilder("format", "experimental_strftime")
.setInvariantType( stringType )
.setExactArgumentCount( 2 )
.setArgumentsValidator( CommonFunctionFactory.formatValidator() )
.setArgumentListSignature("(TEMPORAL datetime as STRING pattern)")
.register();
}

View File

@ -1066,7 +1066,6 @@ public class MySQLDialect extends Dialect {
.replace("D", "%j")
//am pm
.replace("aa", "%p")
.replace("a", "%p")
//hour

View File

@ -1201,7 +1201,6 @@ public class OracleDialect extends Dialect {
.replace("D", fm + "DDD" + fmReset)
//am pm
.replace("aa", "AM")
.replace("a", "AM")
//hour

View File

@ -731,7 +731,6 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
//D no equivalent
//am pm
.replace("aa", "tt")
.replace("a", "tt")
//h nothing to do

View File

@ -423,7 +423,7 @@ public class SpannerDialect extends Dialect {
queryEngine.getSqmFunctionRegistry().patternDescriptorBuilder("format", "format_timestamp(?2,?1)")
.setInvariantType( stringType )
.setExactArgumentCount( 2 )
.setArgumentsValidator( CommonFunctionFactory.formatValidator() )
.setArgumentListSignature("(TIMESTAMP datetime as STRING pattern)")
.register();
}

View File

@ -9,20 +9,28 @@ package org.hibernate.dialect.function;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Types;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Arrays;
import java.util.Locale;
import java.util.function.Supplier;
import org.hibernate.QueryException;
import org.hibernate.dialect.Dialect;
import org.hibernate.metamodel.mapping.BasicValuedMapping;
import org.hibernate.query.ReturnableType;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.ArgumentsValidator;
import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.expression.SqmFormat;
import org.hibernate.query.sqm.tree.expression.SqmLiteral;
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.type.BasicType;
@ -2331,8 +2339,7 @@ public class CommonFunctionFactory {
.setInvariantType(
queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
)
.setExactArgumentCount( 2 )
.setParameterTypes(TEMPORAL, STRING)
.setArgumentsValidator( formatValidator() )
.setArgumentListSignature( "(TEMPORAL datetime as STRING pattern)" )
.register();
}
@ -2347,8 +2354,7 @@ public class CommonFunctionFactory {
.setInvariantType(
queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
)
.setExactArgumentCount( 2 )
.setParameterTypes(TEMPORAL, STRING)
.setArgumentsValidator( formatValidator() )
.setArgumentListSignature( "(TEMPORAL datetime as STRING pattern)" )
.register();
}
@ -2363,8 +2369,7 @@ public class CommonFunctionFactory {
.setInvariantType(
queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
)
.setExactArgumentCount( 2 )
.setParameterTypes(TEMPORAL, STRING)
.setArgumentsValidator( formatValidator() )
.setArgumentListSignature( "(TEMPORAL datetime as STRING pattern)" )
.register();
}
@ -2379,12 +2384,15 @@ public class CommonFunctionFactory {
.setInvariantType(
queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
)
.setExactArgumentCount( 2 )
.setParameterTypes(TEMPORAL, STRING)
.setArgumentsValidator( formatValidator() )
.setArgumentListSignature( "(TEMPORAL datetime as STRING pattern)" )
.register();
}
public static ArgumentsValidator formatValidator() {
return new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), TEMPORAL, STRING );
}
/**
* Use the 'collate' operator which exists on at least Postgres, MySQL, Oracle, and SQL Server
*/

View File

@ -8,8 +8,6 @@ package org.hibernate.dialect.function;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
@ -22,9 +20,6 @@ import org.hibernate.type.spi.TypeConfiguration;
import java.util.List;
import jakarta.persistence.TemporalType;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL;
/**
* DB2's varchar_format() can't handle quoted literal strings in
* the format pattern. So just split the pattern into bits, call
@ -39,7 +34,7 @@ public class DB2FormatEmulation
public DB2FormatEmulation(TypeConfiguration typeConfiguration) {
super(
"format",
new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), TEMPORAL, STRING ),
CommonFunctionFactory.formatValidator(),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
)

View File

@ -22,9 +22,6 @@ import org.hibernate.sql.ast.tree.expression.Format;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.spi.TypeConfiguration;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.TEMPORAL;
/**
* SQL Server behaves strangely when the first argument to format is of the type time, so we cast to datetime.
*
@ -37,7 +34,7 @@ public class SQLServerFormatEmulation extends AbstractSqmSelfRenderingFunctionDe
public SQLServerFormatEmulation(SQLServerDialect dialect, TypeConfiguration typeConfiguration) {
super(
"format",
new ArgumentTypesValidator( StandardArgumentsValidators.exactly( 2 ), TEMPORAL, STRING ),
CommonFunctionFactory.formatValidator(),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( StandardBasicTypes.STRING )
)

View File

@ -3481,34 +3481,9 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
);
}
// G era
// y year in era
// Y week year (ISO)
// M month in year
// w week in year (ISO)
// W week in month
// E day name in week
// e day number in week (*very* inconsistent across DBs)
// d day in month
// D day in year
// a AM/PM
// H hour of day (0-23)
// h clock hour of am/pm (1-12)
// m minute of hour
// s second of minute
// S fraction of second
// z time zone name e.g. PST
// x zone offset e.g. +03, +0300, +03:00
// Z zone offset e.g. +0300
// see https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html
private static final Pattern FORMAT = Pattern.compile("('[^']+'|[:;/,.!@#$^&?~`|()\\[\\]{}<>\\-+*=]|\\s|G{1,2}|[yY]{1,4}|M{1,4}|w{1,2}|W|E{3,4}|e{1,2}|d{1,2}|D{1,3}|a{1,2}|[Hhms]{1,2}|S{1,6}|[zZx]{1,3})*");
@Override
public Object visitFormat(HqlParser.FormatContext ctx) {
String format = QuotingHelper.unquoteStringLiteral( ctx.getChild( 0 ).getText() );
if (!FORMAT.matcher(format).matches()) {
throw new SemanticException("illegal format pattern '" + format + "'");
}
final String format = QuotingHelper.unquoteStringLiteral( ctx.getChild( 0 ).getText() );
return new SqmFormat(
format,
resolveExpressableTypeBasic( String.class ),

View File

@ -6,6 +6,10 @@
*/
package org.hibernate.query.sqm.tree.expression;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import org.hibernate.query.SemanticException;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.SemanticQueryWalker;
import org.hibernate.query.sqm.SqmExpressable;
@ -21,6 +25,19 @@ public class SqmFormat extends SqmLiteral<String> {
SqmExpressable<String> inherentType,
NodeBuilder nodeBuilder) {
super(value, inherentType, nodeBuilder);
try {
DateTimeFormatter.ofPattern( value );
}
catch (IllegalArgumentException ex) {
throw new SemanticException(
String.format(
Locale.ROOT,
"Format [%s] does not conform to the expected format of java.time.DateTimeFormatter!",
value
),
ex
);
}
}
@Override

View File

@ -1253,7 +1253,7 @@ public class FunctionTests {
public void testFormat(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery("select format(e.theTime as 'hh:mm:ss aa') from EntityOfBasics e")
session.createQuery("select format(e.theTime as 'hh:mm:ss a') from EntityOfBasics e")
.list();
session.createQuery("select format(e.theDate as 'dd/MM/yy'), format(e.theDate as 'EEEE, MMMM dd, yyyy') from EntityOfBasics e")
.list();
@ -1265,7 +1265,7 @@ public class FunctionTests {
is("Monday, 25/03/1974")
);
assertThat(
session.createQuery("select format(theTime as '''Hello'', hh:mm:ss aa') from EntityOfBasics where id=123").getResultList().get(0),
session.createQuery("select format(theTime as '''Hello'', hh:mm:ss a') from EntityOfBasics where id=123").getResultList().get(0),
is("Hello, 08:10:08 PM")
);
}

View File

@ -881,7 +881,7 @@ public class StandardFunctionTests {
public void testFormat(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery( "select format(e.theTime as 'hh:mm:ss aa') from EntityOfBasics e" )
session.createQuery( "select format(e.theTime as 'hh:mm:ss a') from EntityOfBasics e" )
.list();
session.createQuery(
"select format(e.theDate as 'dd/MM/yy'), format(e.theDate as 'EEEE, MMMM dd, yyyy') from EntityOfBasics e" )
@ -899,7 +899,7 @@ public class StandardFunctionTests {
);
assertThat(
session.createQuery(
"select format(e.theTime as '''Hello'', hh:mm:ss aa') from EntityOfBasics e" )
"select format(e.theTime as '''Hello'', hh:mm:ss a') from EntityOfBasics e" )
.getResultList()
.get( 0 ),
is( "Hello, 08:10:08 PM" )