diff --git a/elide-core/src/main/java/com/yahoo/elide/core/dictionary/ArgumentType.java b/elide-core/src/main/java/com/yahoo/elide/core/dictionary/ArgumentType.java index 493d03ea68..f73954a810 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/dictionary/ArgumentType.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/dictionary/ArgumentType.java @@ -7,19 +7,24 @@ import com.yahoo.elide.core.type.Type; -import lombok.Getter; +import lombok.Value; /** * Argument Type wraps an argument to the type of value it accepts. */ +@Value public class ArgumentType { - @Getter private String name; - @Getter private Type type; + private Object defaultValue; public ArgumentType(String name, Type type) { + this(name, type, null); + } + + public ArgumentType(String name, Type type, Object defaultValue) { this.name = name; this.type = type; + this.defaultValue = defaultValue; } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidParameterizedAttributeException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidParameterizedAttributeException.java index 25ab7d972e..757fc6194e 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidParameterizedAttributeException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidParameterizedAttributeException.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.core.exceptions; +import com.yahoo.elide.core.request.Argument; import com.yahoo.elide.core.request.Attribute; /** @@ -15,4 +16,9 @@ public InvalidParameterizedAttributeException(Attribute attribute) { super(HttpStatus.SC_BAD_REQUEST, "No attribute found with matching parameters for attribute: " + attribute.toString()); } + + public InvalidParameterizedAttributeException(String attributeName, Argument argument) { + super(HttpStatus.SC_BAD_REQUEST, String.format("Invalid argument : %s for attribute: %s", + argument, attributeName)); + } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/predicates/FilterPredicate.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/predicates/FilterPredicate.java index 52971eefe0..167980265d 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/predicates/FilterPredicate.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/predicates/FilterPredicate.java @@ -43,6 +43,7 @@ public class FilterPredicate implements FilterExpression, Function values; @Getter @NonNull private String field; @Getter @NonNull private String fieldPath; + @Getter @NonNull private Type fieldType; public static boolean toManyInPath(EntityDictionary dictionary, Path path) { return path.getPathElements().stream() @@ -85,6 +86,9 @@ public FilterPredicate(Path path, Operator op, List values) { this.fieldPath = path.getPathElements().stream() .map(PathElement::getFieldName) .collect(Collectors.joining(PERIOD)); + this.fieldType = path.lastElement() + .map(PathElement::getType) + .orElse(null); } /** diff --git a/elide-core/src/main/java/com/yahoo/elide/core/type/ParameterizedModel.java b/elide-core/src/main/java/com/yahoo/elide/core/type/ParameterizedModel.java index 73ce9b4fa5..6abf8a2a42 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/type/ParameterizedModel.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/type/ParameterizedModel.java @@ -6,19 +6,24 @@ package com.yahoo.elide.core.type; +import com.yahoo.elide.annotation.Exclude; import com.yahoo.elide.core.exceptions.InvalidParameterizedAttributeException; import com.yahoo.elide.core.request.Argument; import com.yahoo.elide.core.request.Attribute; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; /** * Base class that contains one or more parameterized attributes. */ public abstract class ParameterizedModel { - private Map parameterizedAttributes; + + @Exclude + protected Map parameterizedAttributes; public ParameterizedModel() { this(new HashMap<>()); @@ -45,9 +50,37 @@ public T invoke(Set arguments) { * @return The attribute value. */ public T invoke(Attribute attribute) { - if (! parameterizedAttributes.containsKey(attribute)) { + Optional match = parameterizedAttributes.keySet().stream() + + //Only filter by alias required. (Filtering by type may not work with inheritance). + .filter((modelAttribute) -> attribute.getAlias().equals(modelAttribute.getAlias())) + .findFirst(); + + if (! match.isPresent()) { throw new InvalidParameterizedAttributeException(attribute); } - return parameterizedAttributes.get(attribute).invoke(attribute.getArguments()); + + return parameterizedAttributes.get(match.get()).invoke(attribute.getArguments()); + } + + /** + * Fetch the attribute value by name. + * @param alias The field name to fetch. + * @param defaultValue Returned if the field name is not found + * @param The return type of the attribute. + * @return The attribute value or the provided default value. + */ + public T fetch(String alias, T defaultValue) { + Optional match = parameterizedAttributes.keySet().stream() + + //Only filter by alias required. (Filtering by type may not work with inheritance). + .filter((modelAttribute) -> alias.equals(modelAttribute.getAlias())) + .findFirst(); + + if (! match.isPresent()) { + return defaultValue; + } + + return parameterizedAttributes.get(match.get()).invoke(new HashSet<>()); } } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/type/ParameterizedModelTest.java b/elide-core/src/test/java/com/yahoo/elide/core/type/ParameterizedModelTest.java new file mode 100644 index 0000000000..b33146ae57 --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/type/ParameterizedModelTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core.type; + +import static com.yahoo.elide.core.type.ClassType.STRING_TYPE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import com.yahoo.elide.core.exceptions.InvalidParameterizedAttributeException; +import com.yahoo.elide.core.request.Attribute; +import org.junit.jupiter.api.Test; + +public class ParameterizedModelTest { + + @Test + public void testInvoke() { + ParameterizedModel testModel = spy(ParameterizedModel.class); + Attribute testAttribute = Attribute.builder().type(STRING_TYPE).name("foo").build(); + String testValue = "bar"; + + testModel.addAttributeValue(testAttribute, testValue); + + assertEquals(testValue, testModel.invoke(testAttribute)); + } + + @Test + public void testInvokeException() { + ParameterizedModel testModel = spy(ParameterizedModel.class); + Attribute testAttribute = Attribute.builder().type(STRING_TYPE).name("foo").build(); + + Exception exception = assertThrows(InvalidParameterizedAttributeException.class, + () -> testModel.invoke(testAttribute)); + + assertEquals("No attribute found with matching parameters for attribute: Attribute(name=foo)", + exception.getMessage()); + } + + @Test + public void testFetch() { + ParameterizedModel testModel = spy(ParameterizedModel.class); + Attribute testAttribute = Attribute.builder().type(STRING_TYPE).name("foo").build(); + String testValue = "bar"; + + testModel.addAttributeValue(testAttribute, testValue); + + assertEquals(testValue, testModel.fetch(testAttribute.getAlias(), "blah")); + } + + @Test + public void testFetchDefault() { + ParameterizedModel testModel = spy(ParameterizedModel.class); + Attribute testAttribute = Attribute.builder().type(STRING_TYPE).name("foo").build(); + String testValue = "blah"; + + assertEquals(testValue, testModel.fetch(testAttribute.getAlias(), testValue)); + } +} diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStore.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStore.java index 2a313aa23e..142d5003b5 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStore.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStore.java @@ -7,12 +7,16 @@ import com.yahoo.elide.core.datastore.DataStore; import com.yahoo.elide.core.datastore.DataStoreTransaction; +import com.yahoo.elide.core.dictionary.ArgumentType; import com.yahoo.elide.core.dictionary.EntityDictionary; +import com.yahoo.elide.core.type.ClassType; import com.yahoo.elide.core.type.Type; import com.yahoo.elide.core.utils.ClassScanner; import com.yahoo.elide.datastores.aggregation.annotation.Join; import com.yahoo.elide.datastores.aggregation.cache.Cache; import com.yahoo.elide.datastores.aggregation.core.QueryLogger; +import com.yahoo.elide.datastores.aggregation.metadata.models.Table; +import com.yahoo.elide.datastores.aggregation.metadata.models.TimeDimension; import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.FromSubquery; import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.FromTable; import lombok.Builder; @@ -57,6 +61,16 @@ public void populateEntityDictionary(EntityDictionary dictionary) { ClassScanner.getAnnotatedClasses(AGGREGATION_STORE_CLASSES).forEach( cls -> dictionary.bindEntity(cls, Collections.singleton(Join.class)) ); + + /* Add 'grain' argument to each TimeDimensionColumn */ + for (Table table : queryEngine.getMetaDataStore().getMetaData(new ClassType<>(Table.class))) { + for (TimeDimension timeDim : table.getTimeDimensions()) { + dictionary.addArgumentToAttribute( + dictionary.getEntityClass(table.getName(), table.getVersion()), + timeDim.getName(), + new ArgumentType("grain", ClassType.STRING_TYPE, timeDim.getDefaultGrain().getGrain())); + } + } } @Override diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/Temporal.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/Temporal.java index 5e29e0497b..2082a88e3d 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/Temporal.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/Temporal.java @@ -28,7 +28,7 @@ * * @return time grain. */ - TimeGrainDefinition grain() default @TimeGrainDefinition(grain = TimeGrain.DAY, expression = "{{}}"); + TimeGrainDefinition[] grains() default { @TimeGrainDefinition(grain = TimeGrain.DAY, expression = "{{}}") }; /** * The timezone in {@link String} of the column. diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStore.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStore.java index 1853dfb175..9bed06d30a 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStore.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStore.java @@ -231,7 +231,10 @@ private void addColumn(Column column) { addMetaData(column, version); if (column instanceof TimeDimension) { - addTimeDimensionGrain(((TimeDimension) column).getSupportedGrain(), version); + TimeDimension timeDimension = (TimeDimension) column; + for (TimeDimensionGrain grain : timeDimension.getSupportedGrains()) { + addTimeDimensionGrain(grain, version); + } } else if (column instanceof Metric) { addMetricFunction(((Metric) column).getMetricFunction(), version); } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimension.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimension.java index 8607200483..c03bd49f3c 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimension.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimension.java @@ -8,11 +8,15 @@ import com.yahoo.elide.annotation.Include; import com.yahoo.elide.core.dictionary.EntityDictionary; import com.yahoo.elide.datastores.aggregation.annotation.Temporal; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.Value; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.TimeZone; +import java.util.stream.Collectors; import javax.persistence.ManyToMany; /** @@ -25,7 +29,7 @@ public class TimeDimension extends Column { @ManyToMany @ToString.Exclude - TimeDimensionGrain supportedGrain; + LinkedHashSet supportedGrains; TimeZone timezone; @@ -38,6 +42,16 @@ public TimeDimension(Table table, String fieldName, EntityDictionary dictionary) Temporal.class, fieldName); - this.supportedGrain = new TimeDimensionGrain(getId(), temporal.grain()); + if (temporal.grains().length == 0) { + this.supportedGrains = new LinkedHashSet<>(Arrays.asList(new TimeDimensionGrain(getId(), TimeGrain.DAY))); + } else { + this.supportedGrains = Arrays.stream(temporal.grains()) + .map(grain -> new TimeDimensionGrain(getId(), grain)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + } + + public TimeDimensionGrain getDefaultGrain() { + return supportedGrains.iterator().next(); } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimensionGrain.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimensionGrain.java index 4309b85ed9..57cdcdabcd 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimensionGrain.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimensionGrain.java @@ -25,9 +25,20 @@ public class TimeDimensionGrain { private final String format; public TimeDimensionGrain(String fieldName, TimeGrainDefinition definition) { - this.id = fieldName + "." + definition.grain().name().toLowerCase(Locale.ENGLISH); + this.id = getId(fieldName, definition.grain()); this.grain = definition.grain(); this.expression = definition.expression(); this.format = definition.grain().getFormat(); } + + public TimeDimensionGrain(String fieldName, TimeGrain grain) { + this.id = getId(fieldName, grain); + this.grain = grain; + this.expression = "{{}}"; + this.format = grain.getFormat(); + } + + private static String getId(String fieldName, TimeGrain grain) { + return fieldName + "." + grain.name().toLowerCase(Locale.ENGLISH); + } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/query/Queryable.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/query/Queryable.java index 2dacf49df6..b86473bb24 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/query/Queryable.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/query/Queryable.java @@ -62,12 +62,12 @@ default String getAlias(String columnName) { /** * Retrieves a column by name. - * @param name The name of the column. + * @param name The alias of the column. * @return The column. */ default ColumnProjection getColumnProjection(String name) { return getColumnProjections().stream() - .filter(dim -> dim.getName().equals(name)) + .filter(dim -> dim.getAlias().equals(name)) .findFirst() .orElse(null); } @@ -79,7 +79,7 @@ default ColumnProjection getColumnProjection(String name) { */ default ColumnProjection getDimensionProjection(String name) { return getDimensionProjections().stream() - .filter(dim -> dim.getName().equals(name)) + .filter(dim -> dim.getAlias().equals(name)) .findFirst() .orElse(null); } @@ -97,7 +97,7 @@ default ColumnProjection getDimensionProjection(String name) { */ default MetricProjection getMetricProjection(String name) { return getMetricProjections().stream() - .filter(metric -> metric.getName().equals(name)) + .filter(metric -> metric.getAlias().equals(name)) .findFirst() .orElse(null); } @@ -115,7 +115,7 @@ default MetricProjection getMetricProjection(String name) { */ default TimeDimensionProjection getTimeDimensionProjection(String name) { return getTimeDimensionProjections().stream() - .filter(dim -> dim.getName().equals(name)) + .filter(dim -> dim.getAlias().equals(name)) .findFirst() .orElse(null); } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/EntityHydrator.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/EntityHydrator.java similarity index 73% rename from elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/EntityHydrator.java rename to elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/EntityHydrator.java index 804fc6b796..0399acc128 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/EntityHydrator.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/EntityHydrator.java @@ -3,10 +3,11 @@ * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ -package com.yahoo.elide.datastores.aggregation.queryengines; +package com.yahoo.elide.datastores.aggregation.queryengines.sql; import com.yahoo.elide.core.dictionary.EntityDictionary; import com.yahoo.elide.core.request.Attribute; +import com.yahoo.elide.core.type.ClassType; import com.yahoo.elide.core.type.ParameterizedModel; import com.yahoo.elide.core.type.Type; import com.yahoo.elide.core.utils.coerce.CoerceUtil; @@ -17,6 +18,16 @@ import com.yahoo.elide.datastores.aggregation.query.MetricProjection; import com.yahoo.elide.datastores.aggregation.query.Query; import com.yahoo.elide.datastores.aggregation.query.Queryable; +import com.yahoo.elide.datastores.aggregation.query.TimeDimensionProjection; +import com.yahoo.elide.datastores.aggregation.timegrains.Day; +import com.yahoo.elide.datastores.aggregation.timegrains.Hour; +import com.yahoo.elide.datastores.aggregation.timegrains.ISOWeek; +import com.yahoo.elide.datastores.aggregation.timegrains.Minute; +import com.yahoo.elide.datastores.aggregation.timegrains.Month; +import com.yahoo.elide.datastores.aggregation.timegrains.Quarter; +import com.yahoo.elide.datastores.aggregation.timegrains.Second; +import com.yahoo.elide.datastores.aggregation.timegrains.Week; +import com.yahoo.elide.datastores.aggregation.timegrains.Year; import com.google.common.base.Preconditions; import org.apache.commons.lang3.mutable.MutableInt; import lombok.AccessLevel; @@ -107,8 +118,8 @@ protected Object coerceObjectToEntity(Map result, MutableInt cou } result.forEach((fieldName, value) -> { - ColumnProjection dim = query.getSource().getColumnProjection(fieldName); - Type fieldType = entityDictionary.getType(entityClass, fieldName); + ColumnProjection dim = query.getColumnProjection(fieldName); + Type fieldType = getType(entityClass, dim); Attribute attribute = projectionToAttribute(dim, fieldType); if (dim != null && dim.getValueType().equals(ValueType.RELATIONSHIP)) { @@ -151,4 +162,33 @@ private Attribute projectionToAttribute(ColumnProjection projection, Type valueT .type(valueType) .build(); } + + private Type getType(Type modelType, ColumnProjection column) { + if (! (column instanceof TimeDimensionProjection)) { + return entityDictionary.getType(modelType, column.getName()); + } + + switch (((TimeDimensionProjection) column).getGrain()) { + case SECOND: + return new ClassType(Second.class); + case MINUTE: + return new ClassType(Minute.class); + case HOUR: + return new ClassType(Hour.class); + case DAY: + return new ClassType(Day.class); + case ISOWEEK: + return new ClassType(ISOWeek.class); + case WEEK: + return new ClassType(Week.class); + case MONTH: + return new ClassType(Month.class); + case QUARTER: + return new ClassType(Quarter.class); + case YEAR: + return new ClassType(Year.class); + default: + throw new IllegalStateException("Invalid grain type"); + } + } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngine.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngine.java index 7f05c31ded..25eda78873 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngine.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngine.java @@ -10,6 +10,7 @@ import com.yahoo.elide.core.filter.predicates.FilterPredicate; import com.yahoo.elide.core.request.Argument; import com.yahoo.elide.core.request.Pagination; +import com.yahoo.elide.core.type.ClassType; import com.yahoo.elide.core.type.Type; import com.yahoo.elide.core.utils.TimedFunction; import com.yahoo.elide.core.utils.coerce.CoerceUtil; @@ -25,7 +26,6 @@ import com.yahoo.elide.datastores.aggregation.query.QueryPlan; import com.yahoo.elide.datastores.aggregation.query.QueryResult; import com.yahoo.elide.datastores.aggregation.query.TimeDimensionProjection; -import com.yahoo.elide.datastores.aggregation.queryengines.EntityHydrator; import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.FromSubquery; import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.FromTable; import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.VersionQuery; @@ -40,6 +40,7 @@ import com.yahoo.elide.datastores.aggregation.queryengines.sql.query.SQLMetricProjection; import com.yahoo.elide.datastores.aggregation.queryengines.sql.query.SQLQuery; import com.yahoo.elide.datastores.aggregation.queryengines.sql.query.SQLTimeDimensionProjection; +import com.yahoo.elide.datastores.aggregation.timegrains.Time; import com.google.common.base.Preconditions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -226,7 +227,7 @@ public QueryResult executeQuery(Query query, Transaction transaction) { stmt = sqlTransaction.initializeStatement(queryString, dataSource); // Supply the query parameters to the query - supplyFilterQueryParameters(query, stmt); + supplyFilterQueryParameters(query, stmt, dialect); // Run the primary query and log the time spent. ResultSet resultSet = runQuery(stmt, queryString, Function.identity()); @@ -250,7 +251,7 @@ private long getPageTotal(Query query, SQLQuery sql, SqlTransaction sqlTransacti NamedParamPreparedStatement stmt = sqlTransaction.initializeStatement(paginationSQL.toString(), dataSource); // Supply the query parameters to the query - supplyFilterQueryParameters(query, stmt); + supplyFilterQueryParameters(query, stmt, dialect); // Run the Pagination query and log the time spent. Long result = CoerceUtil.coerce(runQuery(stmt, paginationSQL.toString(), SINGLE_RESULT_MAPPER), Long.class); @@ -362,8 +363,9 @@ private Query expandMetricQueryPlans(Query query) { * * @param query The client query * @param stmt Customized Prepared Statement + * @param dialect the SQL dialect */ - private void supplyFilterQueryParameters(Query query, NamedParamPreparedStatement stmt) { + private void supplyFilterQueryParameters(Query query, NamedParamPreparedStatement stmt, SQLDialect dialect) { Collection predicates = new ArrayList<>(); if (query.getWhereFilter() != null) { @@ -375,11 +377,16 @@ private void supplyFilterQueryParameters(Query query, NamedParamPreparedStatemen } for (FilterPredicate filterPredicate : predicates) { + boolean isTimeFilter = filterPredicate.getFieldType().equals(new ClassType(Time.class)); if (filterPredicate.getOperator().isParameterized()) { boolean shouldEscape = filterPredicate.isMatchingOperator(); filterPredicate.getParameters().forEach(param -> { try { - stmt.setObject(param.getName(), shouldEscape ? param.escapeMatching() : param.getValue()); + Object value = param.getValue(); + if (isTimeFilter) { + value = dialect.translateTimeToJDBC((Time) value); + } + stmt.setObject(param.getName(), shouldEscape ? param.escapeMatching() : value); } catch (SQLException e) { throw new IllegalStateException(e); } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/dialects/SQLDialect.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/dialects/SQLDialect.java index ac06bd02b2..9a72a92983 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/dialects/SQLDialect.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/dialects/SQLDialect.java @@ -6,6 +6,7 @@ package com.yahoo.elide.datastores.aggregation.queryengines.sql.dialects; import com.yahoo.elide.datastores.aggregation.annotation.JoinType; +import com.yahoo.elide.datastores.aggregation.timegrains.Time; /** * Interface for SQL Dialects used to customize SQL queries for specific persistent storage. @@ -50,4 +51,18 @@ public interface SQLDialect { * @return the keyword for provided Join type. */ String getJoinKeyword(JoinType joinType); + + /** + * Translates Elide's {@link Time} object to the native JDBC date/time object supported + * by the underlying driver. + * + * @param time The elide time object. + * @return A type compatible with JDBC. + */ + default Object translateTimeToJDBC(Time time) { + if (time.isSupportsHour()) { + return new java.sql.Timestamp(time.getTime()); + } + return new java.sql.Date(time.getTime()); + } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLTable.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLTable.java index 68464ff92d..51dd87b258 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLTable.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLTable.java @@ -6,6 +6,7 @@ package com.yahoo.elide.datastores.aggregation.queryengines.sql.metadata; import com.yahoo.elide.core.dictionary.EntityDictionary; +import com.yahoo.elide.core.request.Argument; import com.yahoo.elide.core.type.Type; import com.yahoo.elide.datastores.aggregation.metadata.enums.ColumnType; import com.yahoo.elide.datastores.aggregation.metadata.enums.ValueType; @@ -27,6 +28,7 @@ import lombok.Getter; import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -119,6 +121,10 @@ public Set getDimensionProjections() { @Override public TimeDimensionProjection getTimeDimensionProjection(String fieldName) { + return getTimeDimensionProjection(fieldName, new HashMap<>()); + } + + public TimeDimensionProjection getTimeDimensionProjection(String fieldName, Map arguments) { TimeDimension dimension = super.getTimeDimension(fieldName); if (dimension == null) { return null; @@ -126,7 +132,19 @@ public TimeDimensionProjection getTimeDimensionProjection(String fieldName) { return new SQLTimeDimensionProjection(dimension, dimension.getTimezone(), dimension.getName(), - new HashMap<>()); + arguments); + } + + public TimeDimensionProjection getTimeDimensionProjection(String fieldName, String alias, + Map arguments) { + TimeDimension dimension = super.getTimeDimension(fieldName); + if (dimension == null) { + return null; + } + return new SQLTimeDimensionProjection(dimension, + dimension.getTimezone(), + alias, + arguments); } @Override @@ -161,6 +179,10 @@ public ColumnProjection getColumnProjection(String name) { return null; } + if (column instanceof TimeDimension) { + return getTimeDimensionProjection(name); + } + return new SQLColumnProjection() { @Override diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/QueryTranslator.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/QueryTranslator.java index e0bd8ebe63..f589228540 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/QueryTranslator.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/QueryTranslator.java @@ -202,7 +202,7 @@ private String extractOrderBy(Map sortClauses, Query pl Path.PathElement last = path.lastElement().get(); - SQLColumnProjection projection = fieldToColumnProjection(plan, last.getFieldName()); + SQLColumnProjection projection = fieldToColumnProjection(plan, last.getAlias()); String orderByClause = (plan.getColumnProjections().contains(projection) && dialect.useAliasForOrderByClause()) ? applyQuotes(projection.getAlias()) @@ -296,7 +296,7 @@ private String translateFilterExpression(FilterExpression expression, private String generatePredicatePathReference(Path path, Query query) { Path.PathElement last = path.lastElement().get(); - SQLColumnProjection projection = fieldToColumnProjection(query, last.getFieldName()); + SQLColumnProjection projection = fieldToColumnProjection(query, last.getAlias()); return projection.toSQL(referenceTable); } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLTimeDimensionProjection.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLTimeDimensionProjection.java index 7e7539289d..bfe753ce93 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLTimeDimensionProjection.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLTimeDimensionProjection.java @@ -6,6 +6,7 @@ package com.yahoo.elide.datastores.aggregation.queryengines.sql.query; +import com.yahoo.elide.core.exceptions.InvalidParameterizedAttributeException; import com.yahoo.elide.core.request.Argument; import com.yahoo.elide.datastores.aggregation.metadata.enums.ColumnType; import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; @@ -20,6 +21,7 @@ import lombok.Builder; import lombok.Value; +import java.util.Locale; import java.util.Map; import java.util.TimeZone; @@ -60,7 +62,7 @@ public SQLTimeDimensionProjection(TimeDimension column, this.expression = column.getExpression(); this.name = column.getName(); this.source = (SQLTable) column.getTable(); - this.grain = column.getSupportedGrain(); + this.grain = getGrainFromArguments(arguments, column); this.arguments = arguments; this.alias = alias; this.timeZone = timeZone; @@ -107,4 +109,19 @@ public SQLTimeDimensionProjection withSourceAndExpression(Queryable source, Stri .timeZone(timeZone) .build(); } + + private TimeDimensionGrain getGrainFromArguments(Map arguments, TimeDimension column) { + Argument grainArgument = arguments.get("grain"); + + if (grainArgument == null) { + return column.getDefaultGrain(); + } + + String grainName = grainArgument.getValue().toString().toLowerCase(Locale.ENGLISH); + + return column.getSupportedGrains().stream() + .filter(grain -> grain.getGrain().name().toLowerCase(Locale.ENGLISH).equals(grainName)) + .findFirst() + .orElseThrow(() -> new InvalidParameterizedAttributeException(name, grainArgument)); + } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Day.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Day.java index 827e07cb60..098f8df01d 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Day.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Day.java @@ -1,5 +1,5 @@ /* - * Copyright 2020, Yahoo Inc. + * Copyright 2021, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ @@ -7,47 +7,43 @@ import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter; import com.yahoo.elide.core.utils.coerce.converters.Serde; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; -import java.sql.Date; -import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; /** * Time Grain class for Day. */ -public class Day extends Date { +public class Day extends Time { public static final String FORMAT = "yyyy-MM-dd"; - private static final SimpleDateFormat FORMATTER = new SimpleDateFormat(FORMAT); - public Day(java.util.Date date) { - super(date.getTime()); + public Day(Date date) { + super(date, true, true, true, false, false, false, getSerializer(TimeGrain.DAY)); + } + + public Day(LocalDateTime date) { + super(date, true, true, true, false, false, false, getSerializer(TimeGrain.DAY)); } @ElideTypeConverter(type = Day.class, name = "Day") static public class DaySerde implements Serde { @Override public Day deserialize(Object val) { - - Day date = null; - - try { - if (val instanceof String) { - date = new Day(new Timestamp(FORMATTER.parse((String) val).getTime())); - } else { - date = new Day(FORMATTER.parse(FORMATTER.format(val))); - } - } catch (ParseException e) { - throw new IllegalArgumentException("String must be formatted as " + FORMAT); + if (val instanceof Date) { + return new Day((Date) val); } - - return date; + LocalDate localDate = LocalDate.parse(val.toString(), DateTimeFormatter.ISO_LOCAL_DATE); + LocalDateTime localDateTime = localDate.atTime(0, 0); + return new Day(localDateTime); } @Override public String serialize(Day val) { - return FORMATTER.format(val); + return val.serializer.format(val); } } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Hour.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Hour.java index e94801d7da..65f9daa018 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Hour.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Hour.java @@ -1,5 +1,5 @@ /* - * Copyright 2020, Yahoo Inc. + * Copyright 2021, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ @@ -7,46 +7,43 @@ import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter; import com.yahoo.elide.core.utils.coerce.converters.Serde; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; -import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; /** * Time Grain class for Hour. */ -public class Hour extends Timestamp { +public class Hour extends Time { public static final String FORMAT = "yyyy-MM-dd'T'HH"; - private static final SimpleDateFormat FORMATTER = new SimpleDateFormat(FORMAT); + public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(FORMAT) + .withZone(ZoneOffset.systemDefault()); - public Hour(java.util.Date date) { - super(date.getTime()); + public Hour(Date date) { + super(date, true, true, true, true, false, false, getSerializer(TimeGrain.HOUR)); + } + + public Hour(LocalDateTime date) { + super(date, true, true, true, true, false, false, getSerializer(TimeGrain.HOUR)); } @ElideTypeConverter(type = Hour.class, name = "Hour") static public class HourSerde implements Serde { @Override public Hour deserialize(Object val) { - - Hour date = null; - - try { - if (val instanceof String) { - date = new Hour(new Timestamp(FORMATTER.parse((String) val).getTime())); - } else { - date = new Hour(FORMATTER.parse(FORMATTER.format(val))); - } - } catch (ParseException e) { - throw new IllegalArgumentException("String must be formatted as " + FORMAT); + if (val instanceof Date) { + return new Hour((Date) val); } - - return date; + return new Hour(LocalDateTime.parse(val.toString(), FORMATTER)); } @Override public String serialize(Hour val) { - return FORMATTER.format(val); + return val.serializer.format(val); } } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/ISOWeek.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/ISOWeek.java index 90cab888fa..50f2fceaa5 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/ISOWeek.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/ISOWeek.java @@ -1,5 +1,5 @@ /* - * Copyright 2020, Yahoo Inc. + * Copyright 2021, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ @@ -7,53 +7,47 @@ import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter; import com.yahoo.elide.core.utils.coerce.converters.Serde; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; -import java.sql.Date; -import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; /** * Time Grain class for ISOWeek. */ -public class ISOWeek extends Date { +public class ISOWeek extends Time { public static final String FORMAT = "yyyy-MM-dd"; - private static final SimpleDateFormat FORMATTER = new SimpleDateFormat(FORMAT); - public ISOWeek(java.util.Date date) { - super(date.getTime()); + public ISOWeek(LocalDateTime date) { + super(date, true, true, true, false, false, false, getSerializer(TimeGrain.ISOWEEK)); } @ElideTypeConverter(type = ISOWeek.class, name = "ISOWeek") static public class ISOWeekSerde implements Serde { - private static final SimpleDateFormat WEEKDATE_FORMATTER = new SimpleDateFormat("u"); - @Override public ISOWeek deserialize(Object val) { - - ISOWeek date = null; - - try { - if (val instanceof String) { - date = new ISOWeek(new Timestamp(FORMATTER.parse((String) val).getTime())); - } else { - date = new ISOWeek(FORMATTER.parse(FORMATTER.format(val))); - } - } catch (ParseException e) { - throw new IllegalArgumentException("String must be formatted as " + FORMAT); + LocalDateTime date; + if (val instanceof Date) { + date = LocalDateTime.ofInstant(((Date) val).toInstant(), ZoneOffset.systemDefault()); + } else { + LocalDate localDate = LocalDate.parse(val.toString(), DateTimeFormatter.ISO_LOCAL_DATE); + date = localDate.atTime(0, 0); } - if (!WEEKDATE_FORMATTER.format(date).equals("1")) { + if (date.getDayOfWeek() != DayOfWeek.MONDAY) { throw new IllegalArgumentException("Date string not a Monday"); } - - return date; + return new ISOWeek(date); } @Override public String serialize(ISOWeek val) { - return FORMATTER.format(val); + return val.serializer.format(val); } } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Minute.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Minute.java index a2cb1dc5d5..8c7e7528ff 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Minute.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Minute.java @@ -1,5 +1,5 @@ /* - * Copyright 2020, Yahoo Inc. + * Copyright 2021, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ @@ -7,46 +7,43 @@ import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter; import com.yahoo.elide.core.utils.coerce.converters.Serde; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; -import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; /** * Time Grain class for Minute. */ -public class Minute extends Timestamp { +public class Minute extends Time { public static final String FORMAT = "yyyy-MM-dd'T'HH:mm"; - private static final SimpleDateFormat FORMATTER = new SimpleDateFormat(FORMAT); + public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(FORMAT) + .withZone(ZoneOffset.systemDefault()); - public Minute(java.util.Date date) { - super(date.getTime()); + public Minute(Date date) { + super(date, true, true, true, true, true, false, getSerializer(TimeGrain.MINUTE)); + } + + public Minute(LocalDateTime date) { + super(date, true, true, true, true, true, false, getSerializer(TimeGrain.MINUTE)); } @ElideTypeConverter(type = Minute.class, name = "Minute") static public class MinuteSerde implements Serde { @Override public Minute deserialize(Object val) { - - Minute date = null; - - try { - if (val instanceof String) { - date = new Minute(new Timestamp(FORMATTER.parse((String) val).getTime())); - } else { - date = new Minute(FORMATTER.parse(FORMATTER.format(val))); - } - } catch (ParseException e) { - throw new IllegalArgumentException("String must be formatted as " + FORMAT); + if (val instanceof Date) { + return new Minute((Date) val); } - - return date; + return new Minute(LocalDateTime.parse(val.toString(), FORMATTER)); } @Override public String serialize(Minute val) { - return FORMATTER.format(val); + return val.serializer.format(val); } } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Month.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Month.java index 7cfd4ce190..4d405ff351 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Month.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Month.java @@ -7,47 +7,46 @@ import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter; import com.yahoo.elide.core.utils.coerce.converters.Serde; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; -import java.sql.Date; -import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; /** * Time Grain class for Month. */ -public class Month extends Date { +public class Month extends Time { public static final String FORMAT = "yyyy-MM"; - private static final SimpleDateFormat FORMATTER = new SimpleDateFormat(FORMAT); + public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(FORMAT) + .withZone(ZoneOffset.systemDefault()); - public Month(java.util.Date date) { - super(date.getTime()); + public Month(Date date) { + super(date, true, true, false, false, false, false, getSerializer(TimeGrain.MONTH)); + } + + public Month(LocalDateTime date) { + super(date, true, true, false, false, false, false, getSerializer(TimeGrain.MONTH)); } @ElideTypeConverter(type = Month.class, name = "Month") static public class MonthSerde implements Serde { @Override public Month deserialize(Object val) { - - Month date = null; - - try { - if (val instanceof String) { - date = new Month(new Timestamp(FORMATTER.parse((String) val).getTime())); - } else { - date = new Month(FORMATTER.parse(FORMATTER.format(val))); - } - } catch (ParseException e) { - throw new IllegalArgumentException("String must be formatted as " + FORMAT); + if (val instanceof Date) { + return new Month((Date) val); } - - return date; + YearMonth yearMonth = YearMonth.parse(val.toString(), FORMATTER); + LocalDateTime localDateTime = LocalDateTime.of(yearMonth.getYear(), yearMonth.getMonth(), 1, 0, 0); + return new Month(localDateTime); } @Override public String serialize(Month val) { - return FORMATTER.format(val); + return val.serializer.format(val); } } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Quarter.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Quarter.java index 8e460b2a06..fda000a1d8 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Quarter.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Quarter.java @@ -7,57 +7,51 @@ import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter; import com.yahoo.elide.core.utils.coerce.converters.Serde; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; -import java.sql.Date; -import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; /** * Time Grain class for Quarter. */ -public class Quarter extends Date { +public class Quarter extends Time { public static final String FORMAT = "yyyy-MM"; - private static final SimpleDateFormat FORMATTER = new SimpleDateFormat(FORMAT); + public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(FORMAT) + .withZone(ZoneId.systemDefault()); - public Quarter(java.util.Date date) { - super(date.getTime()); + public Quarter(LocalDateTime date) { + super(date, true, true, false, false, false, false, getSerializer(TimeGrain.QUARTER)); } @ElideTypeConverter(type = Quarter.class, name = "Quarter") static public class QuarterSerde implements Serde { - private static final SimpleDateFormat MONTH_FORMATTER = new SimpleDateFormat("M"); - private static final Set QUARTER_MONTHS = new HashSet<>(Arrays.asList("1", "4", "7", "10")); - @Override public Quarter deserialize(Object val) { - - Quarter date = null; - - try { - if (val instanceof String) { - date = new Quarter(new Timestamp(FORMATTER.parse((String) val).getTime())); - } else { - date = new Quarter(FORMATTER.parse(FORMATTER.format(val))); - } - } catch (ParseException e) { - throw new IllegalArgumentException("String must be formatted as " + FORMAT); + LocalDateTime date; + if (val instanceof Date) { + date = LocalDateTime.ofInstant(((Date) val).toInstant(), ZoneOffset.systemDefault()); + } else { + YearMonth yearMonth = YearMonth.parse(val.toString(), FORMATTER); + date = LocalDateTime.of(yearMonth.getYear(), yearMonth.getMonth(), 1, 0, 0); } - if (!QUARTER_MONTHS.contains(MONTH_FORMATTER.format(date))) { + int month = date.getMonthValue(); + if (month != 1 && month != 4 && month != 7 && month != 10) { throw new IllegalArgumentException("Date string not a quarter month"); } - return date; + return new Quarter(date); } @Override public String serialize(Quarter val) { - return FORMATTER.format(val); + return val.serializer.format(val); } } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Second.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Second.java index 58495bf891..491239d6b0 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Second.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Second.java @@ -7,52 +7,43 @@ import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter; import com.yahoo.elide.core.utils.coerce.converters.Serde; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; -import java.sql.Timestamp; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; /** * Time Grain class for Second. */ -public class Second extends Timestamp { +public class Second extends Time { public static final String FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; - private static final SimpleDateFormat FORMATTER = new SimpleDateFormat(FORMAT); + public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(FORMAT) + .withZone(ZoneOffset.systemDefault()); - - public Second(java.util.Date date) { - super(date.getTime()); + public Second(Date date) { + super(date, true, true, true, true, true, true, getSerializer(TimeGrain.SECOND)); } - @Override - public String toString() { - return FORMATTER.format(this); + public Second(LocalDateTime date) { + super(date, true, true, true, true, true, true, getSerializer(TimeGrain.SECOND)); } @ElideTypeConverter(type = Second.class, name = "Second") static public class SecondSerde implements Serde { @Override public Second deserialize(Object val) { - - Second date = null; - - try { - if (val instanceof String) { - date = new Second(new Timestamp(FORMATTER.parse((String) val).getTime())); - } else { - date = new Second(FORMATTER.parse(FORMATTER.format(val))); - } - } catch (ParseException e) { - throw new IllegalArgumentException("String must be formatted as " + FORMAT); + if (val instanceof Date) { + return new Second((Date) val); } - - return date; + return new Second(LocalDateTime.parse(val.toString(), FORMATTER)); } @Override public String serialize(Second val) { - return FORMATTER.format(val); + return val.serializer.format(val); } } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Time.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Time.java new file mode 100644 index 0000000000..71790b0682 --- /dev/null +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/timegrains/Time.java @@ -0,0 +1,195 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.datastores.aggregation.timegrains; + +import com.yahoo.elide.core.type.ClassType; +import com.yahoo.elide.core.type.Type; +import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter; +import com.yahoo.elide.core.utils.coerce.converters.Serde; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; +import lombok.Data; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.Date; +import java.util.Objects; + +/** + * Time date type for all analytic model time dimensions. + */ +@Data +public class Time extends Date { + + public static Type