diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 41ba76d32..06810d7e0 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -1066,6 +1066,10 @@ public class BeanDescriptor implements BeanType { return owner.getScalarType(jdbcType); } + public ScalarType getScalarType(String cast) { + return owner.getScalarType(cast); + } + /** * Return true if this bean type has a default select clause that is not * simply select all properties. diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 9d8c6fbe6..9b2a26d11 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -265,6 +265,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap { } } + @Override + public ScalarType getScalarType(String cast) { + return typeManager.getScalarType(cast); + } + @Override public ScalarType getScalarType(int jdbcType) { return typeManager.getScalarType(jdbcType); diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java index 9d8387565..90fe0c5ff 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java @@ -66,4 +66,9 @@ public interface BeanDescriptorMap { * Return the scalarType for the given JDBC type. */ ScalarType getScalarType(int jdbcType); + + /** + * Return the scalarType for the given logical type. + */ + ScalarType getScalarType(String cast); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/FormulaPropertyPath.java b/src/main/java/io/ebeaninternal/server/deploy/FormulaPropertyPath.java index cc7d86bc1..38b348162 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/FormulaPropertyPath.java +++ b/src/main/java/io/ebeaninternal/server/deploy/FormulaPropertyPath.java @@ -5,15 +5,11 @@ import io.ebeaninternal.server.query.SqlTreeProperty; import io.ebeaninternal.server.type.ScalarType; import java.sql.Types; -import java.util.regex.Matcher; -import java.util.regex.Pattern; class FormulaPropertyPath { private static final String[] AGG_FUNCTIONS = {"count", "max", "min", "avg"}; - private static final Pattern pattern = Pattern.compile("([a-zA-Z]*)\\((.*)\\)"); - private static final String DISTINCT_ = "distinct "; private final BeanDescriptor descriptor; @@ -26,18 +22,46 @@ class FormulaPropertyPath { private boolean countDistinct; + private String cast; + private String alias; + FormulaPropertyPath(BeanDescriptor descriptor, String formula) { this.descriptor = descriptor; this.formula = formula; - Matcher matcher = pattern.matcher(formula); - if (!matcher.find()) { + int openBracket = formula.indexOf('('); + int closeBracket = formula.lastIndexOf(')'); + if (openBracket == -1 || closeBracket == -1) { throw new IllegalStateException("Unable to parse formula [" + formula + "]"); } - //int groupCount = matcher.groupCount(); - outerFunction = matcher.group(1); - internalExpression = trimDistinct(matcher.group(2)); + outerFunction = formula.substring(0, openBracket).trim(); + internalExpression = trimDistinct(formula.substring(openBracket+1, closeBracket)); + + if (closeBracket < formula.length() -1) { + // ::CastType as foo + String suffix = formula.substring(closeBracket+1).trim(); + parseSuffix(suffix); + } + } + + private void parseSuffix(String suffix) { + String[] split = suffix.split(" "); + if (split.length == 1) { + if (split[0].startsWith("::")) { + cast = split[0].substring(2); + } else { + alias = split[0]; + } + + } else if (split.length == 2) { + cast = "as".equals(split[0]) ? null : split[0].substring(2); + alias = split[1]; + + } else if (split.length == 3) { + cast = split[0].substring(2); + alias = split[2]; + } } private String trimDistinct(String propertyName) { @@ -49,14 +73,21 @@ class FormulaPropertyPath { } } - String aggType() { + String outerFunction() { return outerFunction; } - String basePropertyName() { + String internalExpression() { return internalExpression; } + String cast() { + return cast; + } + + String alias() { + return alias; + } SqlTreeProperty build() { @@ -66,7 +97,13 @@ class FormulaPropertyPath { ElPropertyDeploy firstProp = parser.getFirstProp(); ScalarType scalarType; - if (isCount()) { + if (cast != null) { + scalarType = descriptor.getScalarType(cast); + if (scalarType == null) { + throw new IllegalStateException("Unable to find scalarType for cast of ["+cast+"] on formula [" + formula + "] for type " + descriptor); + } + + } else if (isCount()) { scalarType = descriptor.getScalarType(Types.BIGINT); } else if (isConcat()) { @@ -81,8 +118,14 @@ class FormulaPropertyPath { } } + String logicalName = (alias == null) ? formula : alias; + BeanProperty targetProperty = null; + if (alias != null) { + targetProperty = descriptor._findBeanProperty(alias); + } + String parsedAggregation = buildFormula(parsed); - return new DynamicPropertyAggregationFormula(formula, scalarType, parsedAggregation, isAggregate(), null); + return new DynamicPropertyAggregationFormula(logicalName, scalarType, parsedAggregation, isAggregate(), targetProperty); } private boolean isAggregate() { diff --git a/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index 4ffe6074b..ef11a43c5 100644 --- a/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -83,6 +83,8 @@ public final class DefaultTypeManager implements TypeManager { private final ConcurrentHashMap> nativeMap; + private final ConcurrentHashMap> logicalMap; + private final DefaultTypeFactory extraTypeFactory; private final ScalarType hstoreType = new ScalarTypePostgresHstore(); @@ -179,6 +181,7 @@ public final class DefaultTypeManager implements TypeManager { this.jsonDateTime = config.getJsonDateTime(); this.typeMap = new ConcurrentHashMap<>(); this.nativeMap = new ConcurrentHashMap<>(); + this.logicalMap = new ConcurrentHashMap<>(); boolean objectMapperPresent = config.getClassLoadConfig().isJacksonObjectMapperPresent(); this.objectMapper = (objectMapperPresent) ? initObjectMapper(config) : null; @@ -289,6 +292,11 @@ public final class DefaultTypeManager implements TypeManager { } } + @Override + public ScalarType getScalarType(String cast) { + return logicalMap.get(cast); + } + /** * Return the ScalarType for the given jdbc type as per java.sql.Types. */ @@ -842,30 +850,35 @@ public final class DefaultTypeManager implements TypeManager { if (config.getClassLoadConfig().isJavaTimePresent()) { logger.debug("Registering java.time data types"); - typeMap.put(java.time.Period.class, new ScalarTypePeriod()); - typeMap.put(java.time.LocalDate.class, new ScalarTypeLocalDate()); - typeMap.put(java.time.LocalDateTime.class, new ScalarTypeLocalDateTime(mode)); - typeMap.put(OffsetDateTime.class, new ScalarTypeOffsetDateTime(mode)); - typeMap.put(ZonedDateTime.class, new ScalarTypeZonedDateTime(mode)); - typeMap.put(Instant.class, new ScalarTypeInstant(mode)); + addType(java.time.Period.class, new ScalarTypePeriod()); + addType(java.time.LocalDate.class, new ScalarTypeLocalDate()); + addType(java.time.LocalDateTime.class, new ScalarTypeLocalDateTime(mode)); + addType(OffsetDateTime.class, new ScalarTypeOffsetDateTime(mode)); + addType(ZonedDateTime.class, new ScalarTypeZonedDateTime(mode)); + addType(Instant.class, new ScalarTypeInstant(mode)); - typeMap.put(DayOfWeek.class, new ScalarTypeDayOfWeek()); - typeMap.put(Month.class, new ScalarTypeMonth()); - typeMap.put(Year.class, new ScalarTypeYear()); - typeMap.put(YearMonth.class, new ScalarTypeYearMonthDate()); - typeMap.put(MonthDay.class, new ScalarTypeMonthDay()); - typeMap.put(OffsetTime.class, new ScalarTypeOffsetTime()); - typeMap.put(ZoneId.class, new ScalarTypeZoneId()); - typeMap.put(ZoneOffset.class, new ScalarTypeZoneOffset()); + addType(DayOfWeek.class, new ScalarTypeDayOfWeek()); + addType(Month.class, new ScalarTypeMonth()); + addType(Year.class, new ScalarTypeYear()); + addType(YearMonth.class, new ScalarTypeYearMonthDate()); + addType(MonthDay.class, new ScalarTypeMonthDay()); + addType(OffsetTime.class, new ScalarTypeOffsetTime()); + addType(ZoneId.class, new ScalarTypeZoneId()); + addType(ZoneOffset.class, new ScalarTypeZoneOffset()); boolean localTimeNanos = config.isLocalTimeWithNanos(); - typeMap.put(java.time.LocalTime.class, (localTimeNanos) ? new ScalarTypeLocalTimeWithNanos() : new ScalarTypeLocalTime()); + addType(java.time.LocalTime.class, (localTimeNanos) ? new ScalarTypeLocalTimeWithNanos() : new ScalarTypeLocalTime()); boolean durationNanos = config.isDurationWithNanos(); - typeMap.put(Duration.class, (durationNanos) ? new ScalarTypeDurationWithNanos() : new ScalarTypeDuration()); + addType(Duration.class, (durationNanos) ? new ScalarTypeDurationWithNanos() : new ScalarTypeDuration()); } } + private void addType(Class clazz, ScalarType scalarType) { + typeMap.put(clazz, scalarType); + logicalMap.putIfAbsent(clazz.getSimpleName(), scalarType); + } + /** * Detect if Joda classes are in the classpath and if so register the Joda data types. */ @@ -876,20 +889,20 @@ public final class DefaultTypeManager implements TypeManager { if (config.getClassLoadConfig().isJodaTimePresent()) { // Joda classes are in the classpath so register the types logger.debug("Registering Joda data types"); - typeMap.put(LocalDateTime.class, new ScalarTypeJodaLocalDateTime(mode)); - typeMap.put(DateTime.class, new ScalarTypeJodaDateTime(mode)); - typeMap.put(LocalDate.class, new ScalarTypeJodaLocalDate()); - typeMap.put(org.joda.time.DateMidnight.class, new ScalarTypeJodaDateMidnight()); - typeMap.put(org.joda.time.Period.class, new ScalarTypeJodaPeriod()); + addType(LocalDateTime.class, new ScalarTypeJodaLocalDateTime(mode)); + addType(DateTime.class, new ScalarTypeJodaDateTime(mode)); + addType(LocalDate.class, new ScalarTypeJodaLocalDate()); + addType(org.joda.time.DateMidnight.class, new ScalarTypeJodaDateMidnight()); + addType(org.joda.time.Period.class, new ScalarTypeJodaPeriod()); String jodaLocalTimeMode = config.getJodaLocalTimeMode(); if ("normal".equalsIgnoreCase(jodaLocalTimeMode)) { // use the expected/normal local time zone - typeMap.put(LocalTime.class, new ScalarTypeJodaLocalTime()); + addType(LocalTime.class, new ScalarTypeJodaLocalTime()); logger.debug("registered ScalarTypeJodaLocalTime"); } else if ("utc".equalsIgnoreCase(jodaLocalTimeMode)) { // use the old UTC based - typeMap.put(LocalTime.class, new ScalarTypeJodaLocalTimeUTC()); + addType(LocalTime.class, new ScalarTypeJodaLocalTimeUTC()); logger.debug("registered ScalarTypeJodaLocalTimeUTC"); } } @@ -908,17 +921,17 @@ public final class DefaultTypeManager implements TypeManager { nativeMap.put(DbPlatformType.HSTORE, hstoreType); ScalarType utilDateType = extraTypeFactory.createUtilDate(mode); - typeMap.put(java.util.Date.class, utilDateType); + addType(java.util.Date.class, utilDateType); ScalarType calType = extraTypeFactory.createCalendar(mode); - typeMap.put(Calendar.class, calType); + addType(Calendar.class, calType); ScalarType mathBigIntType = extraTypeFactory.createMathBigInteger(); - typeMap.put(BigInteger.class, mathBigIntType); + addType(BigInteger.class, mathBigIntType); ScalarTypeBool booleanType = extraTypeFactory.createBoolean(); - typeMap.put(Boolean.class, booleanType); - typeMap.put(boolean.class, booleanType); + addType(Boolean.class, booleanType); + addType(boolean.class, booleanType); // register the boolean literals to the platform for DDL default values databasePlatform.setDbTrueLiteral(booleanType.getDbTrueLiteral()); @@ -934,31 +947,31 @@ public final class DefaultTypeManager implements TypeManager { ServerConfig.DbUuid dbUuid = config.getDbTypeConfig().getDbUuid(); if (offlineMigrationGeneration || (databasePlatform.isNativeUuidType() && dbUuid.useNativeType())) { - typeMap.put(UUID.class, new ScalarTypeUUIDNative()); + addType(UUID.class, new ScalarTypeUUIDNative()); } else { // Store UUID as binary(16) or varchar(40) ScalarType uuidType = dbUuid.useBinary() ? new ScalarTypeUUIDBinary(dbUuid.useBinaryOptimized()) : new ScalarTypeUUIDVarchar(); - typeMap.put(UUID.class, uuidType); + addType(UUID.class, uuidType); } - typeMap.put(File.class, fileType); - typeMap.put(InetAddress.class, inetAddressType); - typeMap.put(Locale.class, localeType); - typeMap.put(Currency.class, currencyType); - typeMap.put(TimeZone.class, timeZoneType); - typeMap.put(URL.class, urlType); - typeMap.put(URI.class, uriType); + addType(File.class, fileType); + addType(InetAddress.class, inetAddressType); + addType(Locale.class, localeType); + addType(Currency.class, currencyType); + addType(TimeZone.class, timeZoneType); + addType(URL.class, urlType); + addType(URI.class, uriType); // String types - typeMap.put(char[].class, charArrayType); - typeMap.put(char.class, charType); - typeMap.put(String.class, stringType); + addType(char[].class, charArrayType); + addType(char.class, charType); + addType(String.class, stringType); nativeMap.put(Types.VARCHAR, stringType); nativeMap.put(Types.CHAR, stringType); nativeMap.put(Types.LONGVARCHAR, longVarcharType); // Class - typeMap.put(Class.class, classType); + addType(Class.class, classType); if (platformClobType == Types.CLOB) { nativeMap.put(Types.CLOB, clobType); @@ -972,7 +985,7 @@ public final class DefaultTypeManager implements TypeManager { } // Binary type - typeMap.put(byte[].class, varbinaryType); + addType(byte[].class, varbinaryType); nativeMap.put(Types.BINARY, binaryType); nativeMap.put(Types.VARBINARY, varbinaryType); nativeMap.put(Types.LONGVARBINARY, longVarbinaryType); @@ -989,43 +1002,43 @@ public final class DefaultTypeManager implements TypeManager { } // Number types - typeMap.put(Byte.class, byteType); - typeMap.put(byte.class, byteType); + addType(Byte.class, byteType); + addType(byte.class, byteType); nativeMap.put(Types.TINYINT, byteType); - typeMap.put(Short.class, shortType); - typeMap.put(short.class, shortType); + addType(Short.class, shortType); + addType(short.class, shortType); nativeMap.put(Types.SMALLINT, shortType); - typeMap.put(Integer.class, integerType); - typeMap.put(int.class, integerType); + addType(Integer.class, integerType); + addType(int.class, integerType); nativeMap.put(Types.INTEGER, integerType); - typeMap.put(Long.class, longType); - typeMap.put(long.class, longType); + addType(Long.class, longType); + addType(long.class, longType); nativeMap.put(Types.BIGINT, longType); - typeMap.put(Double.class, doubleType); - typeMap.put(double.class, doubleType); + addType(Double.class, doubleType); + addType(double.class, doubleType); nativeMap.put(Types.FLOAT, doubleType);// no this is not a bug nativeMap.put(Types.DOUBLE, doubleType); - typeMap.put(Float.class, floatType); - typeMap.put(float.class, floatType); + addType(Float.class, floatType); + addType(float.class, floatType); nativeMap.put(Types.REAL, floatType);// no this is not a bug - typeMap.put(BigDecimal.class, bigDecimalType); + addType(BigDecimal.class, bigDecimalType); nativeMap.put(Types.DECIMAL, bigDecimalType); nativeMap.put(Types.NUMERIC, bigDecimalType); // Temporal types - typeMap.put(Time.class, timeType); + addType(Time.class, timeType); nativeMap.put(Types.TIME, timeType); - typeMap.put(Date.class, dateType); + addType(Date.class, dateType); nativeMap.put(Types.DATE, dateType); ScalarType timestampType = new ScalarTypeTimestamp(mode); - typeMap.put(Timestamp.class, timestampType); + addType(Timestamp.class, timestampType); nativeMap.put(Types.TIMESTAMP, timestampType); } diff --git a/src/main/java/io/ebeaninternal/server/type/TypeManager.java b/src/main/java/io/ebeaninternal/server/type/TypeManager.java index 15827cfbe..ebfebc955 100644 --- a/src/main/java/io/ebeaninternal/server/type/TypeManager.java +++ b/src/main/java/io/ebeaninternal/server/type/TypeManager.java @@ -21,6 +21,11 @@ public interface TypeManager { @SuppressWarnings("rawtypes") void addEnumType(ScalarType type, Class myEnumClass); + /** + * Return the scalar type for the given logical type. + */ + ScalarType getScalarType(String cast); + /** * Return the ScalarType for a given jdbc type. * diff --git a/src/test/java/io/ebeaninternal/server/deploy/FormulaPropertyPathTest.java b/src/test/java/io/ebeaninternal/server/deploy/FormulaPropertyPathTest.java index b990d6077..9526619c9 100644 --- a/src/test/java/io/ebeaninternal/server/deploy/FormulaPropertyPathTest.java +++ b/src/test/java/io/ebeaninternal/server/deploy/FormulaPropertyPathTest.java @@ -31,12 +31,50 @@ public class FormulaPropertyPathTest extends BaseTestCase { assertFormula("concat(name,'-end')", "concat", "name,'-end'"); } - private void assertFormula(String input, String aggType, String baseProperty) { + @Test + public void castFormula() { + + assertFormula("concat(name,'-end')::String", "concat", "name,'-end'", "String", null); + } + + @Test + public void cast_javaInstant() { + + assertFormula("max(updtime)::Instant", "max", "updtime", "Instant", null); + } + + @Test + public void alias() { + assertFormula("concat(name,'-end') name", "concat", "name,'-end'", null, "name"); + assertFormula("concat(name,'-end') as name", "concat", "name,'-end'", null, "name"); + } + + @Test + public void castAndAlias() { + assertFormula("concat(name,'-end')::String name", "concat", "name,'-end'", "String", "name"); + assertFormula("concat(name,'-end')::String as name", "concat", "name,'-end'", "String", "name"); + } + + private void assertFormula(String input, String funcName, String expression) { + assertFormula(input, funcName, expression, null, null); + } + + private void assertFormula(String input, String funcName, String expression, String cast, String alias) { FormulaPropertyPath propertyPath = new FormulaPropertyPath(customerDesc, input); - assertThat(propertyPath.basePropertyName()).isEqualTo(baseProperty); - assertThat(propertyPath.aggType()).isEqualTo(aggType); + assertThat(propertyPath.internalExpression()).isEqualTo(expression); + assertThat(propertyPath.outerFunction()).isEqualTo(funcName); + if (cast != null) { + assertThat(propertyPath.cast()).isEqualTo(cast); + } else { + assertThat(propertyPath.cast()).isNull(); + } + if (alias != null) { + assertThat(propertyPath.alias()).isEqualTo(alias); + } else { + assertThat(propertyPath.alias()).isNull(); + } SqlTreeProperty treeProperty = propertyPath.build(); diff --git a/src/test/java/org/tests/query/aggregation/TestAggregationCount.java b/src/test/java/org/tests/query/aggregation/TestAggregationCount.java index 0ace4031c..63732da01 100644 --- a/src/test/java/org/tests/query/aggregation/TestAggregationCount.java +++ b/src/test/java/org/tests/query/aggregation/TestAggregationCount.java @@ -15,6 +15,7 @@ import org.tests.model.tevent.TEventMany; import org.tests.model.tevent.TEventOne; import java.sql.Timestamp; +import java.time.Instant; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -423,4 +424,24 @@ public class TestAggregationCount extends BaseTestCase { assertThat(sql.get(0)).contains("select concat(t0.updtime,', ',t0.first_name) from contact t0"); } + @Test + public void explicitCast() { + + ResetBasicData.reset(); + + LoggedSqlCollector.start(); + + Instant instant = + + Ebean.find(Contact.class) + .select("max(updtime)::Instant") + .where().isNull("phone") + .findSingleAttribute(); + + assertThat(instant).isNotNull(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql.get(0)).contains("select max(t0.updtime) from contact t0 where t0.phone is null"); + } + }