diff --git a/Util/src/main/java/io/deephaven/util/datastructures/CachingSupplier.java b/Util/src/main/java/io/deephaven/util/datastructures/CachingSupplier.java index 0a0880f7896..d36719b1ad0 100644 --- a/Util/src/main/java/io/deephaven/util/datastructures/CachingSupplier.java +++ b/Util/src/main/java/io/deephaven/util/datastructures/CachingSupplier.java @@ -49,4 +49,14 @@ public OUTPUT_TYPE get() { } return cachedResult; } + + public OUTPUT_TYPE getIfCached() { + if (hasCachedResult) { // force a volatile read + if (errorResult != null) { + throw errorResult; + } + return cachedResult; + } + return null; + } } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/DeferredViewTable.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/DeferredViewTable.java index 37816ccd279..c6aaed349db 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/DeferredViewTable.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/DeferredViewTable.java @@ -212,9 +212,7 @@ private PreAndPostFilters applyFilterRenamings(WhereFilter[] filters) { } else if (filter instanceof MatchFilter) { final MatchFilter matchFilter = (MatchFilter) filter; Assert.assertion(myRenames.size() == 1, "Match Filters should only use one column!"); - String newName = myRenames.get(matchFilter.getColumnName()); - Assert.neqNull(newName, "newName"); - final MatchFilter newFilter = matchFilter.renameFilter(newName); + final WhereFilter newFilter = matchFilter.renameFilter(myRenames); newFilter.init(tableReference.getDefinition(), compilationProcessor); preViewFilters.add(newFilter); } else if (filter instanceof ConditionFilter) { diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/ByteRangeFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/ByteRangeFilter.java index d506eee8bc5..49bd613eba2 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/ByteRangeFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/ByteRangeFilter.java @@ -51,18 +51,18 @@ public ByteRangeFilter(String columnName, byte val1, byte val2, boolean lowerInc } } - static WhereFilter makeByteRangeFilter(String columnName, Condition condition, String value) { + static WhereFilter makeByteRangeFilter(String columnName, Condition condition, byte value) { switch (condition) { case LESS_THAN: - return lt(columnName, RangeConditionFilter.parseByteFilter(value)); + return lt(columnName, value); case LESS_THAN_OR_EQUAL: - return leq(columnName, RangeConditionFilter.parseByteFilter(value)); + return leq(columnName, value); case GREATER_THAN: - return gt(columnName, RangeConditionFilter.parseByteFilter(value)); + return gt(columnName, value); case GREATER_THAN_OR_EQUAL: - return geq(columnName, RangeConditionFilter.parseByteFilter(value)); + return geq(columnName, value); default: - throw new IllegalArgumentException("RangeConditionFilter does not support condition " + condition); + throw new IllegalArgumentException("RangeFilter does not support condition " + condition); } } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/CharRangeFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/CharRangeFilter.java index f5d640d13f5..466f58de85a 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/CharRangeFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/CharRangeFilter.java @@ -47,18 +47,18 @@ public CharRangeFilter(String columnName, char val1, char val2, boolean lowerInc } } - static WhereFilter makeCharRangeFilter(String columnName, Condition condition, String value) { + static WhereFilter makeCharRangeFilter(String columnName, Condition condition, char value) { switch (condition) { case LESS_THAN: - return lt(columnName, RangeConditionFilter.parseCharFilter(value)); + return lt(columnName, value); case LESS_THAN_OR_EQUAL: - return leq(columnName, RangeConditionFilter.parseCharFilter(value)); + return leq(columnName, value); case GREATER_THAN: - return gt(columnName, RangeConditionFilter.parseCharFilter(value)); + return gt(columnName, value); case GREATER_THAN_OR_EQUAL: - return geq(columnName, RangeConditionFilter.parseCharFilter(value)); + return geq(columnName, value); default: - throw new IllegalArgumentException("RangeConditionFilter does not support condition " + condition); + throw new IllegalArgumentException("RangeFilter does not support condition " + condition); } } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/DoubleRangeFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/DoubleRangeFilter.java index 1b195e01b0b..edaed1132a9 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/DoubleRangeFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/DoubleRangeFilter.java @@ -63,18 +63,18 @@ public static WhereFilter makeRange(String columnName, String val) { (double) (positiveOrZero ? parsed + offset : parsed - offset), positiveOrZero, !positiveOrZero); } - static WhereFilter makeDoubleRangeFilter(String columnName, Condition condition, String value) { + static WhereFilter makeDoubleRangeFilter(String columnName, Condition condition, double value) { switch (condition) { case LESS_THAN: - return lt(columnName, Double.parseDouble(value)); + return lt(columnName, value); case LESS_THAN_OR_EQUAL: - return leq(columnName, Double.parseDouble(value)); + return leq(columnName, value); case GREATER_THAN: - return gt(columnName, Double.parseDouble(value)); + return gt(columnName, value); case GREATER_THAN_OR_EQUAL: - return geq(columnName, Double.parseDouble(value)); + return geq(columnName, value); default: - throw new IllegalArgumentException("RangeConditionFilter does not support condition " + condition); + throw new IllegalArgumentException("RangeFilter does not support condition " + condition); } } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/FloatRangeFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/FloatRangeFilter.java index 2cdebf0bde7..d06d81f1857 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/FloatRangeFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/FloatRangeFilter.java @@ -59,18 +59,18 @@ public static WhereFilter makeRange(String columnName, String val) { (float) (positiveOrZero ? parsed + offset : parsed - offset), positiveOrZero, !positiveOrZero); } - static WhereFilter makeFloatRangeFilter(String columnName, Condition condition, String value) { + static WhereFilter makeFloatRangeFilter(String columnName, Condition condition, float value) { switch (condition) { case LESS_THAN: - return lt(columnName, Float.parseFloat(value)); + return lt(columnName, value); case LESS_THAN_OR_EQUAL: - return leq(columnName, Float.parseFloat(value)); + return leq(columnName, value); case GREATER_THAN: - return gt(columnName, Float.parseFloat(value)); + return gt(columnName, value); case GREATER_THAN_OR_EQUAL: - return geq(columnName, Float.parseFloat(value)); + return geq(columnName, value); default: - throw new IllegalArgumentException("RangeConditionFilter does not support condition " + condition); + throw new IllegalArgumentException("RangeFilter does not support condition " + condition); } } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/IntRangeFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/IntRangeFilter.java index ee4a345e2a2..cbe4daafef5 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/IntRangeFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/IntRangeFilter.java @@ -51,18 +51,18 @@ public IntRangeFilter(String columnName, int val1, int val2, boolean lowerInclus } } - static WhereFilter makeIntRangeFilter(String columnName, Condition condition, String value) { + static WhereFilter makeIntRangeFilter(String columnName, Condition condition, int value) { switch (condition) { case LESS_THAN: - return lt(columnName, RangeConditionFilter.parseIntFilter(value)); + return lt(columnName, value); case LESS_THAN_OR_EQUAL: - return leq(columnName, RangeConditionFilter.parseIntFilter(value)); + return leq(columnName, value); case GREATER_THAN: - return gt(columnName, RangeConditionFilter.parseIntFilter(value)); + return gt(columnName, value); case GREATER_THAN_OR_EQUAL: - return geq(columnName, RangeConditionFilter.parseIntFilter(value)); + return geq(columnName, value); default: - throw new IllegalArgumentException("RangeConditionFilter does not support condition " + condition); + throw new IllegalArgumentException("RangeFilter does not support condition " + condition); } } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/LongRangeFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/LongRangeFilter.java index d0718eab953..353cb457e4c 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/LongRangeFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/LongRangeFilter.java @@ -51,18 +51,18 @@ public LongRangeFilter(String columnName, long val1, long val2, boolean lowerInc } } - static WhereFilter makeLongRangeFilter(String columnName, Condition condition, String value) { + static WhereFilter makeLongRangeFilter(String columnName, Condition condition, long value) { switch (condition) { case LESS_THAN: - return lt(columnName, RangeConditionFilter.parseLongFilter(value)); + return lt(columnName, value); case LESS_THAN_OR_EQUAL: - return leq(columnName, RangeConditionFilter.parseLongFilter(value)); + return leq(columnName, value); case GREATER_THAN: - return gt(columnName, RangeConditionFilter.parseLongFilter(value)); + return gt(columnName, value); case GREATER_THAN_OR_EQUAL: - return geq(columnName, RangeConditionFilter.parseLongFilter(value)); + return geq(columnName, value); default: - throw new IllegalArgumentException("RangeConditionFilter does not support condition " + condition); + throw new IllegalArgumentException("RangeFilter does not support condition " + condition); } } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/MatchFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/MatchFilter.java index 2144b522bb1..d4026f087aa 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/MatchFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/MatchFilter.java @@ -5,6 +5,7 @@ import io.deephaven.api.literal.Literal; import io.deephaven.base.string.cache.CompressedString; +import io.deephaven.base.verify.Assert; import io.deephaven.engine.liveness.LivenessScopeStack; import io.deephaven.engine.rowset.RowSet; import io.deephaven.engine.rowset.WritableRowSet; @@ -14,13 +15,17 @@ import io.deephaven.engine.table.Table; import io.deephaven.engine.table.TableDefinition; import io.deephaven.engine.table.impl.QueryCompilerRequestProcessor; +import io.deephaven.engine.table.impl.lang.QueryLanguageFunctionUtils; import io.deephaven.engine.table.impl.preview.DisplayWrapper; import io.deephaven.engine.table.impl.DependencyStreamProvider; import io.deephaven.engine.table.impl.indexer.DataIndexer; import io.deephaven.engine.updategraph.NotificationQueue; import io.deephaven.time.DateTimeUtils; +import io.deephaven.util.QueryConstants; import io.deephaven.util.SafeCloseable; +import io.deephaven.util.datastructures.CachingSupplier; import io.deephaven.util.type.ArrayTypeUtils; +import io.deephaven.util.type.TypeUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jpy.PyObject; @@ -28,7 +33,12 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZonedDateTime; import java.util.*; +import java.util.function.Consumer; import java.util.stream.Stream; public class MatchFilter extends WhereFilterImpl implements DependencyStreamProvider { @@ -45,8 +55,11 @@ static MatchFilter ofLiterals( literals.stream().map(AsObject::of).toArray()); } + /** A fail-over WhereFilter supplier should the match filter initialization fail. */ + private final CachingSupplier failoverFilter; + @NotNull - private final String columnName; + private String columnName; private Object[] values; private final String[] strValues; private final boolean invertMatch; @@ -78,7 +91,7 @@ public MatchFilter( @NotNull final MatchType matchType, @NotNull final String columnName, @NotNull final Object... values) { - this(CaseSensitivity.MatchCase, matchType, columnName, null, values); + this(null, CaseSensitivity.MatchCase, matchType, columnName, null, values); } /** @@ -89,30 +102,34 @@ public MatchFilter( public MatchFilter( @NotNull final String columnName, @NotNull final Object... values) { - this(CaseSensitivity.IgnoreCase, MatchType.Regular, columnName, null, values); + this(null, CaseSensitivity.IgnoreCase, MatchType.Regular, columnName, null, values); } public MatchFilter( @NotNull final CaseSensitivity sensitivity, + @NotNull final MatchType matchType, @NotNull final String columnName, @NotNull final String... strValues) { - this(sensitivity, MatchType.Regular, columnName, strValues, null); + this(null, sensitivity, matchType, columnName, strValues, null); } public MatchFilter( + @Nullable final CachingSupplier failoverFilter, @NotNull final CaseSensitivity sensitivity, @NotNull final MatchType matchType, @NotNull final String columnName, @NotNull final String... strValues) { - this(sensitivity, matchType, columnName, strValues, null); + this(failoverFilter, sensitivity, matchType, columnName, strValues, null); } private MatchFilter( + @Nullable final CachingSupplier failoverFilter, @NotNull final CaseSensitivity sensitivity, @NotNull final MatchType matchType, @NotNull final String columnName, @Nullable String[] strValues, @Nullable final Object[] values) { + this.failoverFilter = failoverFilter; this.caseInsensitive = sensitivity == CaseSensitivity.IgnoreCase; this.invertMatch = (matchType == MatchType.Inverted); this.columnName = columnName; @@ -120,20 +137,25 @@ private MatchFilter( this.values = values; } - public MatchFilter renameFilter(String newName) { + private ConditionFilter getFailoverFilterIfCached() { + return failoverFilter != null ? failoverFilter.getIfCached() : null; + } + + public WhereFilter renameFilter(Map renames) { + final String newName = renames.get(columnName); + Assert.neqNull(newName, "newName"); if (strValues == null) { + // when we're constructed with values then there is no failover filter return new MatchFilter(getMatchType(), newName, values); } else { return new MatchFilter( + failoverFilter != null ? new CachingSupplier<>( + () -> failoverFilter.get().renameFilter(renames)) : null, caseInsensitive ? CaseSensitivity.IgnoreCase : CaseSensitivity.MatchCase, - getMatchType(), newName, strValues); + getMatchType(), newName, strValues, null); } } - public String getColumnName() { - return columnName; - } - public Object[] getValues() { return values; } @@ -148,11 +170,25 @@ public MatchType getMatchType() { @Override public List getColumns() { + if (!initialized) { + throw new IllegalStateException("Filter must be initialized to invoke getColumnName"); + } + final WhereFilter failover = getFailoverFilterIfCached(); + if (failover != null) { + return failover.getColumns(); + } return Collections.singletonList(columnName); } @Override public List getColumnArrays() { + if (!initialized) { + throw new IllegalStateException("Filter must be initialized to invoke getColumnArrays"); + } + final WhereFilter failover = getFailoverFilterIfCached(); + if (failover != null) { + return failover.getColumnArrays(); + } return Collections.emptyList(); } @@ -168,47 +204,41 @@ public synchronized void init( if (initialized) { return; } - ColumnDefinition column = tableDefinition.getColumn(columnName); - if (column == null) { - throw new RuntimeException("Column \"" + columnName - + "\" doesn't exist in this table, available columns: " + tableDefinition.getColumnNames()); - } - if (strValues == null) { - initialized = true; - return; - } - final List valueList = new ArrayList<>(); - final Map queryScopeVariables = compilationProcessor.getQueryScopeVariables(); - final ColumnTypeConvertor convertor = - ColumnTypeConvertorFactory.getConvertor(column.getDataType(), column.getName()); - for (String strValue : strValues) { - if (queryScopeVariables.containsKey(strValue)) { - Object paramValue = queryScopeVariables.get(strValue); - if (paramValue != null && paramValue.getClass().isArray()) { - ArrayTypeUtils.ArrayAccessor accessor = ArrayTypeUtils.getArrayAccessor(paramValue); - for (int ai = 0; ai < accessor.length(); ++ai) { - valueList.add(convertor.convertParamValue(accessor.get(ai))); - } - } else if (paramValue != null && Collection.class.isAssignableFrom(paramValue.getClass())) { - for (final Object paramValueMember : (Collection) paramValue) { - valueList.add(convertor.convertParamValue(paramValueMember)); - } + try { + ColumnDefinition column = tableDefinition.getColumn(columnName); + if (column == null) { + if (strValues != null && strValues.length == 1 + && (column = tableDefinition.getColumn(strValues[0])) != null) { + // fix up for the case where column name and variable name were swapped + String tmp = columnName; + columnName = strValues[0]; + strValues[0] = tmp; } else { - valueList.add(convertor.convertParamValue(paramValue)); + throw new RuntimeException("Column \"" + columnName + + "\" doesn't exist in this table, available columns: " + tableDefinition.getColumnNames()); } - } else { - Object convertedValue; - try { - convertedValue = convertor.convertStringLiteral(strValue); - } catch (Throwable t) { - throw new IllegalArgumentException("Failed to convert literal value <" + strValue + - "> for column \"" + columnName + "\" of type " + column.getDataType().getName(), t); - } - valueList.add(convertedValue); + } + if (strValues == null) { + initialized = true; + return; + } + final List valueList = new ArrayList<>(); + final Map queryScopeVariables = compilationProcessor.getQueryScopeVariables(); + final ColumnTypeConvertor convertor = ColumnTypeConvertorFactory.getConvertor(column.getDataType()); + for (String strValue : strValues) { + convertor.convertValue(column, tableDefinition, strValue, queryScopeVariables, valueList::add); + } + values = valueList.toArray(); + } catch (final RuntimeException err) { + if (failoverFilter == null) { + throw err; + } + try { + failoverFilter.get().init(tableDefinition, compilationProcessor); + } catch (final RuntimeException ignored) { + throw err; } } - // values = (Object[])ArrayTypeUtils.toArray(valueList, TypeUtils.getBoxedType(theColumn.getDataType())); - values = valueList.toArray(); initialized = true; } @@ -250,6 +280,11 @@ public Stream getDependencyStream() { @Override public WritableRowSet filter( @NotNull RowSet selection, @NotNull RowSet fullSet, @NotNull Table table, boolean usePrev) { + final WhereFilter failover = getFailoverFilterIfCached(); + if (failover != null) { + return failover.filter(selection, fullSet, table, usePrev); + } + final ColumnSource columnSource = table.getColumnSource(columnName); return columnSource.match(invertMatch, usePrev, caseInsensitive, dataIndex, selection, values); } @@ -258,12 +293,22 @@ public WritableRowSet filter( @Override public WritableRowSet filterInverse( @NotNull RowSet selection, @NotNull RowSet fullSet, @NotNull Table table, boolean usePrev) { + final WhereFilter failover = getFailoverFilterIfCached(); + if (failover != null) { + return failover.filterInverse(selection, fullSet, table, usePrev); + } + final ColumnSource columnSource = table.getColumnSource(columnName); return columnSource.match(!invertMatch, usePrev, caseInsensitive, dataIndex, selection, values); } @Override public boolean isSimpleFilter() { + final WhereFilter failover = getFailoverFilterIfCached(); + if (failover != null) { + return failover.isSimpleFilter(); + } + return true; } @@ -282,56 +327,205 @@ Object convertParamValue(Object paramValue) { } return paramValue; } + + /** + * Convert the string value to the appropriate type for the column. + * + * @param column the column definition + * @param strValue the string value to convert + * @param queryScopeVariables the query scope variables + * @param valueConsumer the consumer for the converted value + * @return whether the value was an array or collection + */ + final boolean convertValue( + @NotNull final ColumnDefinition column, + @NotNull final TableDefinition tableDefinition, + @NotNull final String strValue, + @NotNull final Map queryScopeVariables, + @NotNull final Consumer valueConsumer) { + if (tableDefinition.getColumn(strValue) != null) { + // this is also a column name which needs to take precedence, and we can't convert it + throw new IllegalArgumentException(String.format( + "Failed to convert value <%s> for column \"%s\" of type %s; it is a column name", + strValue, column.getName(), column.getDataType().getName())); + } + if (strValue.endsWith("_") + && tableDefinition.getColumn(strValue.substring(0, strValue.length() - 1)) != null) { + // this also a column array name which needs to take precedence, and we can't convert it + throw new IllegalArgumentException(String.format( + "Failed to convert value <%s> for column \"%s\" of type %s; it is a column array access name", + strValue, column.getName(), column.getDataType().getName())); + } + + if (queryScopeVariables.containsKey(strValue)) { + Object paramValue = queryScopeVariables.get(strValue); + if (paramValue != null && paramValue.getClass().isArray()) { + ArrayTypeUtils.ArrayAccessor accessor = ArrayTypeUtils.getArrayAccessor(paramValue); + for (int ai = 0; ai < accessor.length(); ++ai) { + valueConsumer.accept(convertParamValue(accessor.get(ai))); + } + return true; + } + if (paramValue != null && Collection.class.isAssignableFrom(paramValue.getClass())) { + for (final Object paramValueMember : (Collection) paramValue) { + valueConsumer.accept(convertParamValue(paramValueMember)); + } + return true; + } + valueConsumer.accept(convertParamValue(paramValue)); + return false; + } + + try { + valueConsumer.accept(convertStringLiteral(strValue)); + } catch (Throwable t) { + throw new IllegalArgumentException(String.format( + "Failed to convert literal value <%s> for column \"%s\" of type %s", + strValue, column.getName(), column.getDataType().getName()), t); + } + + return false; + } } public static class ColumnTypeConvertorFactory { - public static ColumnTypeConvertor getConvertor(final Class cls, final String name) { + public static ColumnTypeConvertor getConvertor(final Class cls) { if (cls == byte.class) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str) || "NULL_BYTE".equals(str)) { + return QueryConstants.NULL_BYTE_BOXED; + } return Byte.parseByte(str); } + + @Override + Object convertParamValue(Object paramValue) { + paramValue = super.convertParamValue(paramValue); + if (paramValue instanceof Byte || paramValue == null) { + return paramValue; + } + // noinspection unchecked + final TypeUtils.TypeBoxer boxer = + (TypeUtils.TypeBoxer) TypeUtils.getTypeBoxer(paramValue.getClass()); + return QueryLanguageFunctionUtils.byteCast(boxer.get(paramValue)); + } }; } if (cls == short.class) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str) || "NULL_SHORT".equals(str)) { + return QueryConstants.NULL_SHORT_BOXED; + } return Short.parseShort(str); } + + @Override + Object convertParamValue(Object paramValue) { + paramValue = super.convertParamValue(paramValue); + if (paramValue instanceof Short || paramValue == null) { + return paramValue; + } + // noinspection unchecked + final TypeUtils.TypeBoxer boxer = + (TypeUtils.TypeBoxer) TypeUtils.getTypeBoxer(paramValue.getClass()); + return QueryLanguageFunctionUtils.shortCast(boxer.get(paramValue)); + } }; } if (cls == int.class) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str) || "NULL_INT".equals(str)) { + return QueryConstants.NULL_INT_BOXED; + } return Integer.parseInt(str); } + + @Override + Object convertParamValue(Object paramValue) { + paramValue = super.convertParamValue(paramValue); + if (paramValue instanceof Integer || paramValue == null) { + return paramValue; + } + // noinspection unchecked + final TypeUtils.TypeBoxer boxer = + (TypeUtils.TypeBoxer) TypeUtils.getTypeBoxer(paramValue.getClass()); + return QueryLanguageFunctionUtils.intCast(boxer.get(paramValue)); + } }; } if (cls == long.class) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str) || "NULL_LONG".equals(str)) { + return QueryConstants.NULL_LONG_BOXED; + } return Long.parseLong(str); } + + @Override + Object convertParamValue(Object paramValue) { + paramValue = super.convertParamValue(paramValue); + if (paramValue instanceof Long || paramValue == null) { + return paramValue; + } + // noinspection unchecked + final TypeUtils.TypeBoxer boxer = + (TypeUtils.TypeBoxer) TypeUtils.getTypeBoxer(paramValue.getClass()); + return QueryLanguageFunctionUtils.longCast(boxer.get(paramValue)); + } }; } if (cls == float.class) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str) || "NULL_FLOAT".equals(str)) { + return QueryConstants.NULL_FLOAT_BOXED; + } return Float.parseFloat(str); } + + @Override + Object convertParamValue(Object paramValue) { + paramValue = super.convertParamValue(paramValue); + if (paramValue instanceof Float || paramValue == null) { + return paramValue; + } + // noinspection unchecked + final TypeUtils.TypeBoxer boxer = + (TypeUtils.TypeBoxer) TypeUtils.getTypeBoxer(paramValue.getClass()); + return QueryLanguageFunctionUtils.floatCast(boxer.get(paramValue)); + } }; } if (cls == double.class) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str) || "NULL_DOUBLE".equals(str)) { + return QueryConstants.NULL_DOUBLE_BOXED; + } return Double.parseDouble(str); } + + @Override + Object convertParamValue(Object paramValue) { + paramValue = super.convertParamValue(paramValue); + if (paramValue instanceof Double || paramValue == null) { + return paramValue; + } + // noinspection unchecked + final TypeUtils.TypeBoxer boxer = + (TypeUtils.TypeBoxer) TypeUtils.getTypeBoxer(paramValue.getClass()); + return QueryLanguageFunctionUtils.doubleCast(boxer.get(paramValue)); + } }; } if (cls == Boolean.class) { @@ -339,6 +533,9 @@ Object convertStringLiteral(String str) { @Override Object convertStringLiteral(String str) { // NB: Boolean.parseBoolean(str) doesn't do what we want here - anything not true is false. + if ("null".equals(str) || "NULL_BOOLEAN".equals(str)) { + return QueryConstants.NULL_BOOLEAN; + } if (str.equalsIgnoreCase("true")) { return Boolean.TRUE; } @@ -354,6 +551,9 @@ Object convertStringLiteral(String str) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str) || "NULL_CHAR".equals(str)) { + return QueryConstants.NULL_CHAR_BOXED; + } if (str.length() > 1) { // TODO: #1517 Allow escaping of chars if (str.length() == 3 && ((str.charAt(0) == '\'' && str.charAt(2) == '\'') @@ -366,22 +566,84 @@ Object convertStringLiteral(String str) { } return str.charAt(0); } + + @Override + Object convertParamValue(Object paramValue) { + paramValue = super.convertParamValue(paramValue); + if (paramValue instanceof Character || paramValue == null) { + return paramValue; + } + // noinspection unchecked + final TypeUtils.TypeBoxer boxer = + (TypeUtils.TypeBoxer) TypeUtils.getTypeBoxer(paramValue.getClass()); + return QueryLanguageFunctionUtils.charCast(boxer.get(paramValue)); + } }; } if (cls == BigDecimal.class) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } return new BigDecimal(str); } + + @Override + Object convertParamValue(Object paramValue) { + paramValue = super.convertParamValue(paramValue); + if (paramValue instanceof BigDecimal || paramValue == null) { + return paramValue; + } + if (paramValue instanceof BigInteger) { + return new BigDecimal((BigInteger) paramValue); + } + // noinspection unchecked + final TypeUtils.TypeBoxer boxer = + (TypeUtils.TypeBoxer) TypeUtils.getTypeBoxer(paramValue.getClass()); + final Object boxedValue = boxer.get(paramValue); + if (boxedValue == null) { + return null; + } + if (boxedValue instanceof Number) { + return BigDecimal.valueOf(((Number) boxedValue).doubleValue()); + } + return paramValue; + } }; } if (cls == BigInteger.class) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } return new BigInteger(str); } + + @Override + Object convertParamValue(Object paramValue) { + paramValue = super.convertParamValue(paramValue); + if (paramValue instanceof BigInteger || paramValue == null) { + return paramValue; + } + if (paramValue instanceof BigDecimal) { + return ((BigDecimal) paramValue).toBigInteger(); + } + // noinspection unchecked + final TypeUtils.TypeBoxer boxer = + (TypeUtils.TypeBoxer) TypeUtils.getTypeBoxer(paramValue.getClass()); + final Object boxedValue = boxer.get(paramValue); + if (boxedValue == null) { + return null; + } + if (boxedValue instanceof Number) { + return BigInteger.valueOf(((Number) boxedValue).longValue()); + } + return paramValue; + } }; } if (cls == String.class) { @@ -451,6 +713,9 @@ Object convertParamValue(Object paramValue) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } if (str.charAt(0) != '\'' || str.charAt(str.length() - 1) != '\'') { throw new IllegalArgumentException( "Instant literal not enclosed in single-quotes (\"" + str + "\")"); @@ -459,10 +724,73 @@ Object convertStringLiteral(String str) { } }; } + if (cls == LocalDate.class) { + return new ColumnTypeConvertor() { + @Override + Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } + if (str.charAt(0) != '\'' || str.charAt(str.length() - 1) != '\'') { + throw new IllegalArgumentException( + "LocalDate literal not enclosed in single-quotes (\"" + str + "\")"); + } + return DateTimeUtils.parseLocalDate(str.substring(1, str.length() - 1)); + } + }; + } + if (cls == LocalTime.class) { + return new ColumnTypeConvertor() { + @Override + Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } + if (str.charAt(0) != '\'' || str.charAt(str.length() - 1) != '\'') { + throw new IllegalArgumentException( + "LocalTime literal not enclosed in single-quotes (\"" + str + "\")"); + } + return DateTimeUtils.parseLocalTime(str.substring(1, str.length() - 1)); + } + }; + } + if (cls == LocalDateTime.class) { + return new ColumnTypeConvertor() { + @Override + Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } + if (str.charAt(0) != '\'' || str.charAt(str.length() - 1) != '\'') { + throw new IllegalArgumentException( + "LocalDateTime literal not enclosed in single-quotes (\"" + str + "\")"); + } + return DateTimeUtils.parseLocalDateTime(str.substring(1, str.length() - 1)); + } + }; + } + if (cls == ZonedDateTime.class) { + return new ColumnTypeConvertor() { + @Override + Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } + if (str.charAt(0) != '\'' || str.charAt(str.length() - 1) != '\'') { + throw new IllegalArgumentException( + "ZoneDateTime literal not enclosed in single-quotes (\"" + str + "\")"); + } + return DateTimeUtils.parseZonedDateTime(str.substring(1, str.length() - 1)); + } + }; + } if (cls == Object.class) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } if (str.startsWith("\"") || str.startsWith("`")) { return str.substring(1, str.length() - 1); } else if (str.contains(".")) { @@ -489,6 +817,9 @@ Object convertStringLiteral(String str) { return new ColumnTypeConvertor() { @Override Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } if (str.startsWith("\"") || str.startsWith("`")) { return DisplayWrapper.make(str.substring(1, str.length() - 1)); } else { @@ -497,8 +828,16 @@ Object convertStringLiteral(String str) { } }; } - throw new IllegalArgumentException( - "Unknown type " + cls.getName() + " for MatchFilter value auto-conversion"); + return new ColumnTypeConvertor() { + @Override + Object convertStringLiteral(String str) { + if ("null".equals(str)) { + return null; + } + throw new IllegalArgumentException( + "Can't create " + cls.getName() + " from String Literal for value auto-conversion"); + } + }; } } @@ -544,9 +883,12 @@ public boolean canMemoize() { public WhereFilter copy() { final MatchFilter copy; if (strValues != null) { - copy = new MatchFilter(caseInsensitive ? CaseSensitivity.IgnoreCase : CaseSensitivity.MatchCase, - getMatchType(), columnName, strValues); + copy = new MatchFilter( + failoverFilter == null ? null : new CachingSupplier<>(() -> failoverFilter.get().copy()), + caseInsensitive ? CaseSensitivity.IgnoreCase : CaseSensitivity.MatchCase, + getMatchType(), columnName, strValues, null); } else { + // when we're constructed with values then there is no failover filter copy = new MatchFilter(getMatchType(), columnName, values); } if (initialized) { diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/RangeConditionFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/RangeFilter.java similarity index 53% rename from engine/table/src/main/java/io/deephaven/engine/table/impl/select/RangeConditionFilter.java rename to engine/table/src/main/java/io/deephaven/engine/table/impl/select/RangeFilter.java index f7cdfaa570a..65d65b75e03 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/RangeConditionFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/RangeFilter.java @@ -12,12 +12,18 @@ import io.deephaven.engine.rowset.WritableRowSet; import io.deephaven.engine.rowset.RowSet; import io.deephaven.gui.table.filters.Condition; +import io.deephaven.util.annotations.VisibleForTesting; import io.deephaven.util.type.TypeUtils; +import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.NotNull; import java.math.BigDecimal; import java.math.BigInteger; -import java.util.Collections; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZonedDateTime; import java.util.List; /** @@ -29,11 +35,11 @@ *
  • GREATER_THAN_OR_EQUAL
  • * */ -public class RangeConditionFilter extends WhereFilterImpl { +public class RangeFilter extends WhereFilterImpl { - private final String columnName; - private final Condition condition; - private final String value; + private String columnName; + private String value; + private Condition condition; // The expression prior to being parsed private final String expression; @@ -42,48 +48,48 @@ public class RangeConditionFilter extends WhereFilterImpl { private final FormulaParserConfiguration parserConfiguration; /** - * Creates a RangeConditionFilter. + * Creates a RangeFilter. * * @param columnName the column to filter * @param condition the condition for filtering * @param value a String representation of the numeric filter value */ - public RangeConditionFilter(String columnName, Condition condition, String value) { + public RangeFilter(String columnName, Condition condition, String value) { this(columnName, condition, value, null, null, null); } /** - * Creates a RangeConditionFilter. + * Creates a RangeFilter. * * @param columnName the column to filter * @param condition the condition for filtering * @param value a String representation of the numeric filter value * @param expression the original expression prior to being parsed - * @param parserConfiguration + * @param parserConfiguration the parser configuration to use */ - public RangeConditionFilter(String columnName, Condition condition, String value, String expression, + public RangeFilter(String columnName, Condition condition, String value, String expression, FormulaParserConfiguration parserConfiguration) { this(columnName, condition, value, expression, null, parserConfiguration); } /** - * Creates a RangeConditionFilter. + * Creates a RangeFilter. * * @param columnName the column to filter * @param conditionString the String representation of a condition for filtering * @param value a String representation of the numeric filter value * @param expression the original expression prior to being parsed - * @param parserConfiguration + * @param parserConfiguration the parser configuration to use */ - public RangeConditionFilter(String columnName, String conditionString, String value, String expression, + public RangeFilter(String columnName, String conditionString, String value, String expression, FormulaParserConfiguration parserConfiguration) { this(columnName, conditionFromString(conditionString), value, expression, parserConfiguration); } // Used for copy method - private RangeConditionFilter(String columnName, Condition condition, String value, String expression, + private RangeFilter(String columnName, Condition condition, String value, String expression, WhereFilter filter, FormulaParserConfiguration parserConfiguration) { - Assert.eqTrue(conditionSupported(condition), condition + " is not supported by RangeConditionFilter"); + Assert.eqTrue(conditionSupported(condition), condition + " is not supported by RangeFilter"); this.columnName = columnName; this.condition = condition; this.value = value; @@ -115,18 +121,29 @@ private static Condition conditionFromString(String conditionString) { case ">=": return Condition.GREATER_THAN_OR_EQUAL; default: - throw new IllegalArgumentException(conditionString + " is not supported by RangeConditionFilter"); + throw new IllegalArgumentException(conditionString + " is not supported by RangeFilter"); } } @Override public List getColumns() { - return Collections.singletonList(columnName); + if (filter == null) { + throw new IllegalStateException("Filter must be initialized to invoke getColumnName"); + } + return filter.getColumns(); } @Override public List getColumnArrays() { - return Collections.emptyList(); + if (filter == null) { + throw new IllegalStateException("Filter must be initialized to invoke getColumnArrays"); + } + return filter.getColumnArrays(); + } + + @VisibleForTesting + public WhereFilter getRealFilter() { + return filter; } @Override @@ -142,47 +159,104 @@ public void init( return; } - final ColumnDefinition def = tableDefinition.getColumn(columnName); + RuntimeException conversionError = null; + ColumnDefinition def = tableDefinition.getColumn(columnName); if (def == null) { - throw new RuntimeException("Column \"" + columnName + "\" doesn't exist in this table, available columns: " - + tableDefinition.getColumnNames()); + if ((def = tableDefinition.getColumn(value)) != null) { + // fix up for the case where column name and variable name were swapped + String tmp = columnName; + columnName = value; + value = tmp; + condition = condition.mirror(); + } else { + conversionError = new RuntimeException("Column \"" + columnName + + "\" doesn't exist in this table, available columns: " + tableDefinition.getColumnNames()); + } } - final Class colClass = def.getDataType(); + final Class colClass = def == null ? null : def.getDataType(); + final MutableObject realValue = new MutableObject<>(); + + if (def != null) { + final MatchFilter.ColumnTypeConvertor convertor = + MatchFilter.ColumnTypeConvertorFactory.getConvertor(def.getDataType()); + + try { + boolean wasAnArrayType = convertor.convertValue( + def, tableDefinition, value, compilationProcessor.getQueryScopeVariables(), + realValue::setValue); + if (wasAnArrayType) { + conversionError = + new IllegalArgumentException("RangeFilter does not support array types for column " + + columnName + " with value <" + value + ">"); + } + } catch (final RuntimeException err) { + conversionError = err; + } + } - if (colClass == double.class || colClass == Double.class) { - filter = DoubleRangeFilter.makeDoubleRangeFilter(columnName, condition, value); + if (conversionError != null) { + if (expression != null) { + try { + filter = ConditionFilter.createConditionFilter(expression, parserConfiguration); + } catch (final RuntimeException ignored) { + throw conversionError; + } + } else { + throw conversionError; + } + } else if (colClass == double.class || colClass == Double.class) { + filter = DoubleRangeFilter.makeDoubleRangeFilter(columnName, condition, + TypeUtils.unbox((Double) realValue.getValue())); } else if (colClass == float.class || colClass == Float.class) { - filter = FloatRangeFilter.makeFloatRangeFilter(columnName, condition, value); + filter = FloatRangeFilter.makeFloatRangeFilter(columnName, condition, + TypeUtils.unbox((Float) realValue.getValue())); } else if (colClass == char.class || colClass == Character.class) { - filter = CharRangeFilter.makeCharRangeFilter(columnName, condition, value); + filter = CharRangeFilter.makeCharRangeFilter(columnName, condition, + TypeUtils.unbox((Character) realValue.getValue())); } else if (colClass == byte.class || colClass == Byte.class) { - filter = ByteRangeFilter.makeByteRangeFilter(columnName, condition, value); + filter = ByteRangeFilter.makeByteRangeFilter(columnName, condition, + TypeUtils.unbox((Byte) realValue.getValue())); } else if (colClass == short.class || colClass == Short.class) { - filter = ShortRangeFilter.makeShortRangeFilter(columnName, condition, value); + filter = ShortRangeFilter.makeShortRangeFilter(columnName, condition, + TypeUtils.unbox((Short) realValue.getValue())); } else if (colClass == int.class || colClass == Integer.class) { - filter = IntRangeFilter.makeIntRangeFilter(columnName, condition, value); + filter = IntRangeFilter.makeIntRangeFilter(columnName, condition, + TypeUtils.unbox((Integer) realValue.getValue())); } else if (colClass == long.class || colClass == Long.class) { - filter = LongRangeFilter.makeLongRangeFilter(columnName, condition, value); - } else if (io.deephaven.util.type.TypeUtils.isDateTime(colClass)) { - filter = makeDateTimeRangeFilter(columnName, condition, value); + filter = LongRangeFilter.makeLongRangeFilter(columnName, condition, + TypeUtils.unbox((Long) realValue.getValue())); + } else if (colClass == Instant.class) { + filter = makeInstantRangeFilter(columnName, condition, + DateTimeUtils.epochNanos((Instant) realValue.getValue())); + } else if (colClass == LocalDate.class) { + filter = makeComparableRangeFilter(columnName, condition, (LocalDate) realValue.getValue()); + } else if (colClass == LocalTime.class) { + filter = makeComparableRangeFilter(columnName, condition, (LocalTime) realValue.getValue()); + } else if (colClass == LocalDateTime.class) { + filter = makeComparableRangeFilter(columnName, condition, (LocalDateTime) realValue.getValue()); + } else if (colClass == ZonedDateTime.class) { + filter = makeComparableRangeFilter(columnName, condition, (ZonedDateTime) realValue.getValue()); } else if (BigDecimal.class.isAssignableFrom(colClass)) { - filter = makeComparableRangeFilter(columnName, condition, new BigDecimal(value)); + filter = makeComparableRangeFilter(columnName, condition, (BigDecimal) realValue.getValue()); } else if (BigInteger.class.isAssignableFrom(colClass)) { - filter = makeComparableRangeFilter(columnName, condition, new BigInteger(value)); + filter = makeComparableRangeFilter(columnName, condition, (BigInteger) realValue.getValue()); } else if (io.deephaven.util.type.TypeUtils.isString(colClass)) { - final String stringValue = MatchFilter.ColumnTypeConvertorFactory.getConvertor(String.class, columnName) - .convertStringLiteral(value).toString(); - filter = makeComparableRangeFilter(columnName, condition, stringValue); + filter = makeComparableRangeFilter(columnName, condition, (String) realValue.getValue()); } else if (TypeUtils.isBoxedBoolean(colClass) || colClass == boolean.class) { - filter = makeComparableRangeFilter(columnName, condition, Boolean.valueOf(value)); + filter = makeComparableRangeFilter(columnName, condition, (Boolean) realValue.getValue()); } else { // The expression looks like a comparison of number, string, or boolean // but the type does not match (or the column type is misconfigured) if (expression != null) { - filter = ConditionFilter.createConditionFilter(expression, parserConfiguration); + try { + filter = ConditionFilter.createConditionFilter(expression, parserConfiguration); + } catch (final RuntimeException ignored) { + throw new IllegalArgumentException("RangeFilter does not support type " + + colClass.getSimpleName() + " for column " + columnName); + } } else { - throw new IllegalArgumentException("RangeConditionFilter does not support type " + throw new IllegalArgumentException("RangeFilter does not support type " + colClass.getSimpleName() + " for column " + columnName); } } @@ -190,52 +264,19 @@ public void init( filter.init(tableDefinition, compilationProcessor); } - public static char parseCharFilter(String value) { - if (value.startsWith("'") && value.endsWith("'") && value.length() == 3) { - return value.charAt(1); - } - if (value.startsWith("\"") && value.endsWith("\"") && value.length() == 3) { - return value.charAt(1); - } - return (char) Long.parseLong(value); - } - - public static byte parseByteFilter(String value) { - return Byte.parseByte(value); - } - - public static short parseShortFilter(String value) { - return Short.parseShort(value); - } - - public static int parseIntFilter(String value) { - return Integer.parseInt(value); - } - - public static long parseLongFilter(String value) { - return Long.parseLong(value); - } - - private static LongRangeFilter makeDateTimeRangeFilter(String columnName, Condition condition, String value) { + private static LongRangeFilter makeInstantRangeFilter(String columnName, Condition condition, long value) { switch (condition) { case LESS_THAN: - return new InstantRangeFilter(columnName, parseInstantNanos(value), Long.MIN_VALUE, true, false); + return new InstantRangeFilter(columnName, value, Long.MIN_VALUE, true, false); case LESS_THAN_OR_EQUAL: - return new InstantRangeFilter(columnName, parseInstantNanos(value), Long.MIN_VALUE, true, true); + return new InstantRangeFilter(columnName, value, Long.MIN_VALUE, true, true); case GREATER_THAN: - return new InstantRangeFilter(columnName, parseInstantNanos(value), Long.MAX_VALUE, false, true); + return new InstantRangeFilter(columnName, value, Long.MAX_VALUE, false, true); case GREATER_THAN_OR_EQUAL: - return new InstantRangeFilter(columnName, parseInstantNanos(value), Long.MAX_VALUE, true, true); + return new InstantRangeFilter(columnName, value, Long.MAX_VALUE, true, true); default: - throw new IllegalArgumentException("RangeConditionFilter does not support condition " + condition); - } - } - - private static long parseInstantNanos(String value) { - if (value.startsWith("'") && value.endsWith("'")) { - return DateTimeUtils.epochNanos(DateTimeUtils.parseInstant(value.substring(1, value.length() - 1))); + throw new IllegalArgumentException("RangeFilter does not support condition " + condition); } - return Long.parseLong(value); } private static SingleSidedComparableRangeFilter makeComparableRangeFilter(String columnName, Condition condition, @@ -250,7 +291,7 @@ private static SingleSidedComparableRangeFilter makeComparableRangeFilter(String case GREATER_THAN_OR_EQUAL: return new SingleSidedComparableRangeFilter(columnName, comparable, true, true); default: - throw new IllegalArgumentException("RangeConditionFilter does not support condition " + condition); + throw new IllegalArgumentException("RangeFilter does not support condition " + condition); } } @@ -270,7 +311,7 @@ public WritableRowSet filterInverse( @Override public boolean isSimpleFilter() { - return true; + return filter.isSimpleFilter(); } @Override @@ -278,11 +319,12 @@ public void setRecomputeListener(RecomputeListener listener) {} @Override public WhereFilter copy() { - return new RangeConditionFilter(columnName, condition, value, expression, filter, parserConfiguration); + final WhereFilter innerCopy = filter == null ? null : filter.copy(); + return new RangeFilter(columnName, condition, value, expression, innerCopy, parserConfiguration); } @Override public String toString() { - return "RangeConditionFilter(" + columnName + " " + condition.description + " " + value + ")"; + return "RangeFilter(" + columnName + " " + condition.description + " " + value + ")"; } } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/ShortRangeFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/ShortRangeFilter.java index 461707e7b7f..6257a2275b7 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/ShortRangeFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/ShortRangeFilter.java @@ -51,18 +51,18 @@ public ShortRangeFilter(String columnName, short val1, short val2, boolean lower } } - static WhereFilter makeShortRangeFilter(String columnName, Condition condition, String value) { + static WhereFilter makeShortRangeFilter(String columnName, Condition condition, short value) { switch (condition) { case LESS_THAN: - return lt(columnName, RangeConditionFilter.parseShortFilter(value)); + return lt(columnName, value); case LESS_THAN_OR_EQUAL: - return leq(columnName, RangeConditionFilter.parseShortFilter(value)); + return leq(columnName, value); case GREATER_THAN: - return gt(columnName, RangeConditionFilter.parseShortFilter(value)); + return gt(columnName, value); case GREATER_THAN_OR_EQUAL: - return geq(columnName, RangeConditionFilter.parseShortFilter(value)); + return geq(columnName, value); default: - throw new IllegalArgumentException("RangeConditionFilter does not support condition " + condition); + throw new IllegalArgumentException("RangeFilter does not support condition " + condition); } } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilter.java index dbe6b627903..5c17955ccde 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilter.java @@ -87,6 +87,8 @@ interface RecomputeListener { /** * Get the columns required by this select filter. + *

    + * This filter must already be initialized before calling this method. * * @return the columns used as input by this select filter. */ @@ -94,6 +96,8 @@ interface RecomputeListener { /** * Get the array columns required by this select filter. + *

    + * This filter must already be initialized before calling this method. * * @return the columns used as array input by this select filter. */ diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilterAdapter.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilterAdapter.java index 70c6c040974..8ec35409e0a 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilterAdapter.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilterAdapter.java @@ -241,8 +241,8 @@ public PreferredLhsColumnRhsVisitor(ColumnName lhs) { this.lhs = Objects.requireNonNull(lhs); } - // The String vs non-String cases are separated out, as it's necessary in the RangeConditionFilter case to - // wrap String literals with quotes (as that's what RangeConditionFilter expects wrt parsing). MatchFilter + // The String vs non-String cases are separated out, as it's necessary in the RangeFilter case to + // wrap String literals with quotes (as that's what RangeFilter expects wrt parsing). MatchFilter // allows us to pass in the already parsed Object (otherwise, if we were passing strValues we would need to // wrap them) @@ -368,34 +368,34 @@ public WhereFilter visit(RawString rawString) { return original(); } - private RangeConditionFilter range(Object rhsLiteral) { + private RangeFilter range(Object rhsLiteral) { // TODO(deephaven-core#3730): More efficient io.deephaven.api.filter.FilterComparison to RangeFilter final String rhsLiteralAsStr = rhsLiteral.toString(); switch (preferred.operator()) { case LESS_THAN: - return new RangeConditionFilter(lhs.name(), Condition.LESS_THAN, rhsLiteralAsStr); + return new RangeFilter(lhs.name(), Condition.LESS_THAN, rhsLiteralAsStr); case LESS_THAN_OR_EQUAL: - return new RangeConditionFilter(lhs.name(), Condition.LESS_THAN_OR_EQUAL, rhsLiteralAsStr); + return new RangeFilter(lhs.name(), Condition.LESS_THAN_OR_EQUAL, rhsLiteralAsStr); case GREATER_THAN: - return new RangeConditionFilter(lhs.name(), Condition.GREATER_THAN, rhsLiteralAsStr); + return new RangeFilter(lhs.name(), Condition.GREATER_THAN, rhsLiteralAsStr); case GREATER_THAN_OR_EQUAL: - return new RangeConditionFilter(lhs.name(), Condition.GREATER_THAN_OR_EQUAL, rhsLiteralAsStr); + return new RangeFilter(lhs.name(), Condition.GREATER_THAN_OR_EQUAL, rhsLiteralAsStr); } throw new IllegalStateException("Unexpected"); } - private RangeConditionFilter range(String rhsLiteral) { + private RangeFilter range(String rhsLiteral) { // TODO(deephaven-core#3730): More efficient io.deephaven.api.filter.FilterComparison to RangeFilter final String quotedRhsLiteral = '"' + rhsLiteral + '"'; switch (preferred.operator()) { case LESS_THAN: - return new RangeConditionFilter(lhs.name(), Condition.LESS_THAN, quotedRhsLiteral); + return new RangeFilter(lhs.name(), Condition.LESS_THAN, quotedRhsLiteral); case LESS_THAN_OR_EQUAL: - return new RangeConditionFilter(lhs.name(), Condition.LESS_THAN_OR_EQUAL, quotedRhsLiteral); + return new RangeFilter(lhs.name(), Condition.LESS_THAN_OR_EQUAL, quotedRhsLiteral); case GREATER_THAN: - return new RangeConditionFilter(lhs.name(), Condition.GREATER_THAN, quotedRhsLiteral); + return new RangeFilter(lhs.name(), Condition.GREATER_THAN, quotedRhsLiteral); case GREATER_THAN_OR_EQUAL: - return new RangeConditionFilter(lhs.name(), Condition.GREATER_THAN_OR_EQUAL, quotedRhsLiteral); + return new RangeFilter(lhs.name(), Condition.GREATER_THAN_OR_EQUAL, quotedRhsLiteral); } throw new IllegalStateException("Unexpected"); } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilterFactory.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilterFactory.java index c021504f702..11ff3114cdf 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilterFactory.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/select/WhereFilterFactory.java @@ -7,7 +7,6 @@ import io.deephaven.api.filter.FilterPattern; import io.deephaven.api.filter.FilterPattern.Mode; import io.deephaven.base.Pair; -import io.deephaven.engine.context.QueryScope; import io.deephaven.api.expression.AbstractExpressionFactory; import io.deephaven.engine.table.ColumnDefinition; import io.deephaven.engine.table.TableDefinition; @@ -21,6 +20,7 @@ import io.deephaven.io.logger.Logger; import io.deephaven.time.DateTimeUtils; import io.deephaven.util.annotations.VisibleForTesting; +import io.deephaven.util.datastructures.CachingSupplier; import io.deephaven.util.text.SplitIgnoreQuotes; import org.jetbrains.annotations.NotNull; @@ -48,93 +48,28 @@ public class WhereFilterFactory { private static final ExpressionParser parser = new ExpressionParser<>(); static { - // == - // = - // != + // Each side may fit: (||) + // Supported ops: ==, =, !=, <, <=, >, >= parser.registerFactory(new AbstractExpressionFactory<>( - START_PTRN + "(" + ID_PTRN + ")\\s*(?:(?:={1,2})|(!=))\\s*(" + LITERAL_PTRN + ")" + END_PTRN) { + START_PTRN + "(?:(" + ID_PTRN + ")|(" + LITERAL_PTRN + "))\\s*((?:=|!|<|>)=?)\\s*(?:(" + ID_PTRN + ")|(" + + LITERAL_PTRN + "))" + END_PTRN) { @Override public WhereFilter getExpression(String expression, Matcher matcher, Object... args) { - final String columnName = matcher.group(1); - final boolean inverted = matcher.group(2) != null; - final String value = matcher.group(3); - - final FormulaParserConfiguration parserConfiguration = (FormulaParserConfiguration) args[0]; - if (isRowVariable(columnName)) { - log.debug().append("WhereFilterFactory creating ConditionFilter for expression: ") - .append(expression).endl(); - return ConditionFilter.createConditionFilter(expression, parserConfiguration); - } - log.debug().append("WhereFilterFactory creating MatchFilter for expression: ").append(expression) - .endl(); - return new MatchFilter( - MatchFilter.CaseSensitivity.MatchCase, - inverted ? MatchFilter.MatchType.Inverted : MatchFilter.MatchType.Regular, - columnName, - value); - } - }); - // == - // = - // != - parser.registerFactory(new AbstractExpressionFactory<>( - START_PTRN + "(" + ID_PTRN + ")\\s*(?:(?:={1,2})|(!=))\\s*(" + ID_PTRN + ")" + END_PTRN) { - @Override - public WhereFilter getExpression(String expression, Matcher matcher, Object... args) { - final String columnName = matcher.group(1); - final boolean inverted = matcher.group(2) != null; - final String paramName = matcher.group(3); - - final FormulaParserConfiguration parserConfiguration = (FormulaParserConfiguration) args[0]; + // LITERAL_PTRN has 5 groups; mostly non-capturing + final boolean leftIsId = matcher.group(1) != null; + final boolean rightIsId = matcher.group(8) != null; - if (isRowVariable(columnName)) { - log.debug().append("WhereFilterFactory creating ConditionFilter for expression: ") - .append(expression).endl(); - return ConditionFilter.createConditionFilter(expression, parserConfiguration); + if (!leftIsId && !rightIsId) { + return ConditionFilter.createConditionFilter(expression, (FormulaParserConfiguration) args[0]); } - try { - QueryScope.getParamValue(paramName); - } catch (QueryScope.MissingVariableException e) { - return ConditionFilter.createConditionFilter(expression, parserConfiguration); - } - log.debug().append("WhereFilterFactory creating MatchFilter for expression: ").append(expression) - .endl(); - return new MatchFilter( - MatchFilter.CaseSensitivity.MatchCase, - inverted ? MatchFilter.MatchType.Inverted : MatchFilter.MatchType.Regular, - columnName, - paramName); - } - }); + final boolean mirrored = !leftIsId; - // < - // <= - // > - // >= - parser.registerFactory(new AbstractExpressionFactory<>( - START_PTRN + "(" + ID_PTRN + ")\\s*([<>]=?)\\s*(" + LITERAL_PTRN + ")" + END_PTRN) { - @Override - public WhereFilter getExpression(String expression, Matcher matcher, Object... args) { - final FormulaParserConfiguration parserConfiguration = (FormulaParserConfiguration) args[0]; - final String columnName = matcher.group(1); - final String conditionString = matcher.group(2); - final String value = matcher.group(3); - if (isRowVariable(columnName)) { - log.debug().append("WhereFilterFactory creating ConditionFilter for expression: ") - .append(expression).endl(); - return ConditionFilter.createConditionFilter(expression, parserConfiguration); - } - try { - log.debug().append("WhereFilterFactory creating RangeConditionFilter for expression: ") - .append(expression).endl(); - return new RangeConditionFilter(columnName, conditionString, value, expression, - parserConfiguration); - } catch (Exception e) { - log.warn().append("WhereFilterFactory could not make RangeFilter for expression: ") - .append(expression).append(" due to ").append(e) - .append(" Creating ConditionFilter instead.").endl(); - return ConditionFilter.createConditionFilter(expression, parserConfiguration); - } + final String columnName = leftIsId ? matcher.group(1) : matcher.group(8); + final String op = matcher.group(7); + final String value = leftIsId ? rightIsId ? matcher.group(8) : matcher.group(9) : matcher.group(2); + + return getWhereFilterOneSideColumn( + expression, (FormulaParserConfiguration) args[0], columnName, op, value, mirrored); } }); @@ -202,6 +137,67 @@ public WhereFilter getExpression(String expression, Matcher matcher, Object... a }); } + private static @NotNull WhereFilter getWhereFilterOneSideColumn( + final String expression, + final FormulaParserConfiguration parserConfiguration, + final String columnName, + String op, + final String value, + boolean mirrored) { + + if (isRowVariable(columnName)) { + log.debug().append("WhereFilterFactory creating ConditionFilter for expression: ") + .append(expression).endl(); + return ConditionFilter.createConditionFilter(expression, parserConfiguration); + } + + boolean inverted = false; + switch (op) { + case "!=": + inverted = true; + case "=": + case "==": + log.debug().append("WhereFilterFactory creating MatchFilter for expression: ").append(expression) + .endl(); + return new MatchFilter( + new CachingSupplier<>(() -> (ConditionFilter) ConditionFilter.createConditionFilter(expression, + parserConfiguration)), + CaseSensitivity.MatchCase, + inverted ? MatchType.Inverted : MatchType.Regular, + columnName, + value); + + case "<": + case ">": + case "<=": + case ">=": + if (mirrored) { + switch (op) { + case "<": + op = ">"; + break; + case "<=": + op = ">="; + break; + case ">": + op = "<"; + break; + case ">=": + op = "<="; + break; + default: + throw new IllegalStateException("Unexpected operator: " + op); + } + } + log.debug().append("WhereFilterFactory creating RangeFilter for expression: ") + .append(expression).endl(); + return new RangeFilter(columnName, op, value, expression, parserConfiguration); + + default: + throw new IllegalStateException("Unexpected operator: " + op); + } + } + private static boolean isRowVariable(String columnName) { return columnName.equals("i") || columnName.equals("ii") || columnName.equals("k"); } @@ -396,7 +392,7 @@ public static WhereFilter stringContainsFilter( boolean removeQuotes, String... values) { final String value = - constructStringContainsRegex(values, matchType, internalDisjunctive, removeQuotes, columnName); + constructStringContainsRegex(values, matchType, internalDisjunctive, removeQuotes); return WhereFilterAdapter.of(FilterPattern.of( ColumnName.of(columnName), Pattern.compile(value, sensitivity == CaseSensitivity.IgnoreCase ? Pattern.CASE_INSENSITIVE : 0), @@ -408,14 +404,13 @@ private static String constructStringContainsRegex( String[] values, MatchType matchType, boolean internalDisjunctive, - boolean removeQuotes, - String columnName) { + boolean removeQuotes) { if (values == null || values.length == 0) { throw new IllegalArgumentException( "constructStringContainsRegex must be called with at least one value parameter"); } final MatchFilter.ColumnTypeConvertor converter = removeQuotes - ? MatchFilter.ColumnTypeConvertorFactory.getConvertor(String.class, columnName) + ? MatchFilter.ColumnTypeConvertorFactory.getConvertor(String.class) : null; final String regex; final Stream valueStream = Arrays.stream(values) diff --git a/engine/table/src/main/java/io/deephaven/gui/table/filters/Condition.java b/engine/table/src/main/java/io/deephaven/gui/table/filters/Condition.java index 0c29d40e598..8c7a3c6ab51 100644 --- a/engine/table/src/main/java/io/deephaven/gui/table/filters/Condition.java +++ b/engine/table/src/main/java/io/deephaven/gui/table/filters/Condition.java @@ -20,16 +20,46 @@ public enum Condition { NOT_INCLUDES_MATCH_CASE("not includes (casesen)", false), // Numbers and Dates - LESS_THAN("less than", false), - GREATER_THAN("greater than", false), - LESS_THAN_OR_EQUAL("less than or equal to", false), - GREATER_THAN_OR_EQUAL("greater than or equal to", false), + LESS_THAN("less than", false) { + @Override + public Condition mirror() { + return Condition.GREATER_THAN; + } + }, + GREATER_THAN("greater than", false) { + @Override + public Condition mirror() { + return Condition.LESS_THAN; + } + }, + LESS_THAN_OR_EQUAL("less than or equal to", false) { + @Override + public Condition mirror() { + return Condition.GREATER_THAN_OR_EQUAL; + } + }, + GREATER_THAN_OR_EQUAL("greater than or equal to", false) { + @Override + public Condition mirror() { + return Condition.LESS_THAN_OR_EQUAL; + } + }, // Numbers EQUALS_ABS("equals (abs)", true), NOT_EQUALS_ABS("not equals (abs)", false), - LESS_THAN_ABS("less than (abs)", false), - GREATER_THAN_ABS("greater than (abs)", false), + LESS_THAN_ABS("less than (abs)", false) { + @Override + public Condition mirror() { + return Condition.GREATER_THAN_ABS; + } + }, + GREATER_THAN_ABS("greater than (abs)", false) { + @Override + public Condition mirror() { + return Condition.LESS_THAN_ABS; + } + }, // Lists INCLUDED_IN("included in list", true), @@ -45,4 +75,8 @@ public enum Condition { this.description = description; this.defaultOr = defaultOr; } + + public Condition mirror() { + return this; + } } diff --git a/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryFactory.java b/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryFactory.java index 62c25a31b03..1e40d00084e 100644 --- a/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryFactory.java +++ b/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryFactory.java @@ -664,7 +664,7 @@ private String createWhereFilter(Random random) { switch (columnTypes[colNum].getSimpleName()) { case "Instant": - filter.append(colName).append(" > ").append(random.nextInt(1000) * 1_000_000_000L); + filter.append(colName).append(" > '").append(random.nextInt(1000) * 1_000_000_000L).append("'"); break; case "String": diff --git a/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableWhereTest.java b/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableWhereTest.java index dafc65cfe97..acfa9686b6f 100644 --- a/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableWhereTest.java +++ b/engine/table/src/test/java/io/deephaven/engine/table/impl/QueryTableWhereTest.java @@ -32,20 +32,25 @@ import io.deephaven.engine.testutil.generator.*; import io.deephaven.engine.testutil.junit4.EngineCleanup; import io.deephaven.engine.util.TableTools; +import io.deephaven.gui.table.filters.Condition; import io.deephaven.internal.log.LoggerFactory; import io.deephaven.io.logger.Logger; import io.deephaven.time.DateTimeUtils; import io.deephaven.util.QueryConstants; import io.deephaven.util.SafeCloseable; import io.deephaven.util.annotations.ReflexiveUse; +import io.deephaven.util.datastructures.CachingSupplier; import junit.framework.TestCase; +import org.apache.commons.lang3.mutable.MutableBoolean; import org.apache.commons.lang3.mutable.MutableObject; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.Random; import java.util.concurrent.CountDownLatch; @@ -1152,6 +1157,19 @@ public void testComparableBinarySearch() { QueryScope.addParam("nine", null); } + @Test + public void testZonedDateRangeFilter() { + final ZonedDateTime startTime = DateTimeUtils.parseZonedDateTime("2021-04-23T09:30 NY"); + final ZonedDateTime[] array = new ZonedDateTime[10]; + for (int ii = 0; ii < array.length; ++ii) { + array[ii] = DateTimeUtils.plus(startTime, 60_000_000_000L * ii); + } + final Table table = TableTools.newTable(col("ZDT", array)); + showWithRowSet(table); + + testRangeFilterHelper(table, "ZDT", array[5]); + } + @Test public void testInstantRangeFilter() { final Instant startTime = DateTimeUtils.parseInstant("2021-04-23T09:30 NY"); @@ -1162,11 +1180,7 @@ public void testInstantRangeFilter() { final Table table = TableTools.newTable(col("DT", array)); showWithRowSet(table); - final Table sorted = table.sort("DT"); - final Table backwards = table.sort("DT"); - - assertTableEquals(sorted.where("DT < '" + array[5] + "'"), sorted.where("ii < 5")); - assertTableEquals(backwards.where("DT < '" + array[5] + "'"), backwards.where("ii < 5")); + testRangeFilterHelper(table, "DT", array[5]); } @Test @@ -1184,22 +1198,26 @@ public void testCharRangeFilter() { final Table table = TableTools.newTable(charCol("CH", array)); showWithRowSet(table); - final Table sorted = table.sort("CH"); - final Table backwards = table.sort("CH"); + testRangeFilterHelper(table, "CH", array[5]); + } + + private void testRangeFilterHelper(Table table, String name, T mid) { + final Table sorted = table.sort(name); + final Table backwards = table.sort(name); showWithRowSet(sorted); - log.debug().append("Pivot: " + array[5]).endl(); + log.debug().append("Pivot: " + mid).endl(); - final Table rangeFiltered = sorted.where("CH < '" + array[5] + "'"); - final Table standardFiltered = sorted.where("'" + array[5] + "' > CH"); + final Table rangeFiltered = sorted.where(name + " < '" + mid + "'"); + final Table standardFiltered = sorted.where("'" + mid + "' > " + name); showWithRowSet(rangeFiltered); showWithRowSet(standardFiltered); assertTableEquals(rangeFiltered, standardFiltered); - assertTableEquals(backwards.where("CH < '" + array[5] + "'"), backwards.where("'" + array[5] + "' > CH")); - assertTableEquals(backwards.where("CH <= '" + array[5] + "'"), backwards.where("'" + array[5] + "' >= CH")); - assertTableEquals(backwards.where("CH > '" + array[5] + "'"), backwards.where("'" + array[5] + "' < CH")); - assertTableEquals(backwards.where("CH >= '" + array[5] + "'"), backwards.where("'" + array[5] + "' <= CH")); + assertTableEquals(backwards.where(name + " < '" + mid + "'"), backwards.where("'" + mid + "' > " + name)); + assertTableEquals(backwards.where(name + " <= '" + mid + "'"), backwards.where("'" + mid + "' >= " + name)); + assertTableEquals(backwards.where(name + " > '" + mid + "'"), backwards.where("'" + mid + "' < " + name)); + assertTableEquals(backwards.where(name + " >= '" + mid + "'"), backwards.where("'" + mid + "' <= " + name)); } @Test @@ -1351,4 +1369,262 @@ public void testFilterErrorUpdate() { // The where result should have failed, because the filter expression is invalid for the new data. Assert.eqTrue(whereResult.isFailed(), "whereResult.isFailed()"); } + + @Test + public void testMatchFilterFallback() { + final Table table = emptyTable(10).update("X=i"); + ExecutionContext.getContext().getQueryScope().putParam("var1", 10); + ExecutionContext.getContext().getQueryScope().putParam("var2", 20); + + final MutableBoolean called = new MutableBoolean(false); + final MatchFilter filter = new MatchFilter( + new CachingSupplier<>(() -> { + called.setValue(true); + return (ConditionFilter) ConditionFilter.createConditionFilter("var1 != var2"); + }), + MatchFilter.CaseSensitivity.IgnoreCase, MatchFilter.MatchType.Inverted, "var1", "var2"); + + final Table result = table.where(filter); + assertTableEquals(table, result); + + Assert.eqTrue(called.booleanValue(), "called.booleanValue()"); + } + + @Test + public void testRangeFilterFallback() { + final Table table = emptyTable(10).update("X=i"); + ExecutionContext.getContext().getQueryScope().putParam("var1", 10); + ExecutionContext.getContext().getQueryScope().putParam("var2", 20); + + final RangeFilter filter = new RangeFilter( + "0", Condition.LESS_THAN, "var2", "0 < var2", FormulaParserConfiguration.parser); + + final Table result = table.where(filter); + assertTableEquals(table, result); + + final WhereFilter realFilter = filter.getRealFilter(); + Assert.eqTrue(realFilter instanceof ConditionFilter, "realFilter instanceof ConditionFilter"); + } + + @Test + public void testEnsureColumnsTakePrecedence() { + final Table table = emptyTable(10).update("X=i", "Y=i%2"); + ExecutionContext.getContext().getQueryScope().putParam("Y", 5); + + { + final Table r1 = table.where("X == Y"); + final Table r2 = table.where("Y == X"); + Assert.equals(r1.getRowSet(), "r1.getRowSet()", RowSetFactory.flat(2)); + assertTableEquals(r1, r2); + } + + { + final Table r1 = table.where("X >= Y"); + final Table r2 = table.where("Y <= X"); + Assert.equals(r1.getRowSet(), "r1.getRowSet()", RowSetFactory.flat(10)); + assertTableEquals(r1, r2); + } + + { + final Table r1 = table.where("X > Y"); + final Table r2 = table.where("Y < X"); + Assert.equals(r1.getRowSet(), "r1.getRowSet()", RowSetFactory.fromRange(2, 9)); + assertTableEquals(r1, r2); + } + + { + final Table r1 = table.where("X < Y"); + final Table r2 = table.where("Y > X"); + Assert.equals(r1.getRowSet(), "r1.getRowSet()", RowSetFactory.empty()); + assertTableEquals(r1, r2); + } + + { + final Table r1 = table.where("X <= Y"); + final Table r2 = table.where("Y >= X"); + Assert.equals(r1.getRowSet(), "r1.getRowSet()", RowSetFactory.flat(2)); + assertTableEquals(r1, r2); + } + } + + @Test + @Ignore + public void testEnsureColumnArraysTakePrecedence() { + // TODO: column arrays aren't well supported in match arrays and this example's where filter fails to compile + final Table table = emptyTable(10).update("X=i", "Y=new int[]{1, 5, 9}"); + ExecutionContext.getContext().getQueryScope().putParam("Y_", new int[] {0, 4, 8}); + + final Table result = table.where("X == Y_[1]"); + Assert.equals(result.getRowSet(), "result.getRowSet()", RowSetFactory.fromKeys(5)); + + // check that the mirror matches the expected result + final Table mResult = table.where("Y_[1] == X"); + assertTableEquals(result, mResult); + } + + @Test + public void testIntToByteCoercion() { + final Table table = emptyTable(11).update("X = ii % 2 == 0 ? (byte) ii : null"); + final Class colType = table.getDefinition().getColumn("X").getDataType(); + Assert.eq(colType, "colType", byte.class); + + ExecutionContext.getContext().getQueryScope().putParam("real_null", null); + ExecutionContext.getContext().getQueryScope().putParam("val_null", QueryConstants.NULL_INT); + ExecutionContext.getContext().getQueryScope().putParam("val_5", 5); + + final Table real_null_result = table.where("X == real_null"); + final Table null_result = table.where("X == val_null"); + Assert.eq(null_result.size(), "null_result.size()", 5); + assertTableEquals(real_null_result, null_result); + + final Table range_result = table.where("X >= val_5"); + Assert.eq(range_result.size(), "range_result.size()", 3); + } + + @Test + public void testIntToShortCoercion() { + final Table table = emptyTable(11).update("X= ii % 2 == 0 ? (short) ii : null"); + final Class colType = table.getDefinition().getColumn("X").getDataType(); + Assert.eq(colType, "colType", short.class); + + ExecutionContext.getContext().getQueryScope().putParam("real_null", null); + ExecutionContext.getContext().getQueryScope().putParam("val_null", QueryConstants.NULL_INT); + ExecutionContext.getContext().getQueryScope().putParam("val_5", 5); + + final Table real_null_result = table.where("X == real_null"); + final Table null_result = table.where("X == val_null"); + Assert.eq(null_result.size(), "null_result.size()", 5); + assertTableEquals(real_null_result, null_result); + + final Table range_result = table.where("X >= val_5"); + Assert.eq(range_result.size(), "range_result.size()", 3); + } + + @Test + public void testLongToIntCoercion() { + final Table table = emptyTable(11).update("X= ii % 2 == 0 ? (int) ii : null"); + final Class colType = table.getDefinition().getColumn("X").getDataType(); + Assert.eq(colType, "colType", int.class); + + ExecutionContext.getContext().getQueryScope().putParam("real_null", null); + ExecutionContext.getContext().getQueryScope().putParam("val_null", QueryConstants.NULL_LONG); + ExecutionContext.getContext().getQueryScope().putParam("val_5", 5L); + + final Table real_null_result = table.where("X == real_null"); + final Table null_result = table.where("X == val_null"); + Assert.eq(null_result.size(), "null_result.size()", 5); + assertTableEquals(real_null_result, null_result); + + final Table range_result = table.where("X >= val_5"); + Assert.eq(range_result.size(), "range_result.size()", 3); + } + + @Test + public void testIntToLongCoercion() { + final Table table = emptyTable(11).update("X= ii % 2 == 0 ? ii : null"); + final Class colType = table.getDefinition().getColumn("X").getDataType(); + Assert.eq(colType, "colType", long.class); + + ExecutionContext.getContext().getQueryScope().putParam("real_null", null); + ExecutionContext.getContext().getQueryScope().putParam("val_null", QueryConstants.NULL_INT); + ExecutionContext.getContext().getQueryScope().putParam("val_5", 5); + + final Table real_null_result = table.where("X == real_null"); + final Table null_result = table.where("X == val_null"); + Assert.eq(null_result.size(), "null_result.size()", 5); + assertTableEquals(real_null_result, null_result); + + final Table range_result = table.where("X >= val_5"); + Assert.eq(range_result.size(), "range_result.size()", 3); + } + + @Test + public void testIntToFloatCoercion() { + final Table table = emptyTable(11).update("X= ii % 2 == 0 ? (float) ii : null"); + final Class colType = table.getDefinition().getColumn("X").getDataType(); + Assert.eq(colType, "colType", float.class); + + ExecutionContext.getContext().getQueryScope().putParam("real_null", null); + ExecutionContext.getContext().getQueryScope().putParam("val_null", QueryConstants.NULL_INT); + ExecutionContext.getContext().getQueryScope().putParam("val_5", 5); + + final Table real_null_result = table.where("X == real_null"); + final Table null_result = table.where("X == val_null"); + Assert.eq(null_result.size(), "null_result.size()", 5); + assertTableEquals(real_null_result, null_result); + + final Table range_result = table.where("X >= val_5"); + Assert.eq(range_result.size(), "range_result.size()", 3); + } + + @Test + public void testIntToDoubleCoercion() { + final Table table = emptyTable(11).update("X= ii % 2 == 0 ? (double) ii : null"); + final Class colType = table.getDefinition().getColumn("X").getDataType(); + Assert.eq(colType, "colType", double.class); + + ExecutionContext.getContext().getQueryScope().putParam("real_null", null); + ExecutionContext.getContext().getQueryScope().putParam("val_null", QueryConstants.NULL_INT); + ExecutionContext.getContext().getQueryScope().putParam("val_5", 5); + + final Table real_null_result = table.where("X == real_null"); + final Table null_result = table.where("X == val_null"); + Assert.eq(null_result.size(), "null_result.size()", 5); + assertTableEquals(real_null_result, null_result); + + final Table range_result = table.where("X >= val_5"); + Assert.eq(range_result.size(), "range_result.size()", 3); + } + + @Test + public void testBigIntegerCoercion() { + ExecutionContext.getContext().getQueryLibrary().importClass(BigInteger.class); + + final Table table = emptyTable(11).update("X= ii % 2 == 0 ? BigInteger.valueOf(ii) : null"); + final Class colType = table.getDefinition().getColumn("X").getDataType(); + Assert.eq(colType, "colType", BigInteger.class); + + ExecutionContext.getContext().getQueryScope().putParam("real_null", null); + ExecutionContext.getContext().getQueryScope().putParam("val_null", QueryConstants.NULL_INT); + ExecutionContext.getContext().getQueryScope().putParam("val_5", 5); + + final Table real_null_result = table.where("X == real_null"); + final Table null_result = table.where("X == val_null"); + Assert.eq(null_result.size(), "null_result.size()", 5); + assertTableEquals(real_null_result, null_result); + + final Table range_result = table.where("X >= val_5"); + Assert.eq(range_result.size(), "range_result.size()", 3); + + // let's also test BigDecimal -> BigInteger conversion; note that conversion does not round + ExecutionContext.getContext().getQueryScope().putParam("bd_5", BigDecimal.valueOf(5.8)); + final Table bd_result = table.where("X >= bd_5"); + assertTableEquals(range_result, bd_result); + } + + @Test + public void testBigDecimalCoercion() { + ExecutionContext.getContext().getQueryLibrary().importClass(BigDecimal.class); + + final Table table = emptyTable(11).update("X= ii % 2 == 0 ? BigDecimal.valueOf(ii) : null"); + final Class colType = table.getDefinition().getColumn("X").getDataType(); + Assert.eq(colType, "colType", BigDecimal.class); + + ExecutionContext.getContext().getQueryScope().putParam("real_null", null); + ExecutionContext.getContext().getQueryScope().putParam("val_null", QueryConstants.NULL_INT); + ExecutionContext.getContext().getQueryScope().putParam("val_5", 5); + + final Table real_null_result = table.where("X == real_null"); + final Table null_result = table.where("X == val_null"); + Assert.eq(null_result.size(), "null_result.size()", 5); + assertTableEquals(real_null_result, null_result); + + final Table range_result = table.where("X >= val_5"); + Assert.eq(range_result.size(), "range_result.size()", 3); + + // let's also test BigInteger -> BigDecimal conversion + ExecutionContext.getContext().getQueryScope().putParam("bi_5", BigInteger.valueOf(5)); + final Table bi_result = table.where("X >= bi_5"); + assertTableEquals(range_result, bi_result); + } } diff --git a/engine/table/src/test/java/io/deephaven/engine/table/impl/select/WhereFilterTest.java b/engine/table/src/test/java/io/deephaven/engine/table/impl/select/WhereFilterTest.java index 4ce85050b27..f1900291d51 100644 --- a/engine/table/src/test/java/io/deephaven/engine/table/impl/select/WhereFilterTest.java +++ b/engine/table/src/test/java/io/deephaven/engine/table/impl/select/WhereFilterTest.java @@ -73,13 +73,13 @@ public void testEq() { regular(FilterComparison.eq(V42, FOO), MatchFilter.class, "Foo in [42]"); regular(FilterComparison.eq(FOO, HELLO), MatchFilter.class, "Foo in [Hello]"); regular(FilterComparison.eq(HELLO, FOO), MatchFilter.class, "Foo in [Hello]"); - regular(FilterComparison.eq(FOO, BAR), ConditionFilter.class, "Foo == Bar"); + regular(FilterComparison.eq(FOO, BAR), MatchFilter.class, "Foo in [Bar]"); inverse(FilterComparison.eq(FOO, V42), MatchFilter.class, "Foo not in [42]"); inverse(FilterComparison.eq(V42, FOO), MatchFilter.class, "Foo not in [42]"); inverse(FilterComparison.eq(FOO, HELLO), MatchFilter.class, "Foo not in [Hello]"); inverse(FilterComparison.eq(HELLO, FOO), MatchFilter.class, "Foo not in [Hello]"); - inverse(FilterComparison.eq(FOO, BAR), ConditionFilter.class, "Foo != Bar"); + inverse(FilterComparison.eq(FOO, BAR), MatchFilter.class, "Foo not in [Bar]"); } public void testNeq() { @@ -87,101 +87,101 @@ public void testNeq() { regular(FilterComparison.neq(V42, FOO), MatchFilter.class, "Foo not in [42]"); regular(FilterComparison.neq(FOO, HELLO), MatchFilter.class, "Foo not in [Hello]"); regular(FilterComparison.neq(HELLO, FOO), MatchFilter.class, "Foo not in [Hello]"); - regular(FilterComparison.neq(FOO, BAR), ConditionFilter.class, "Foo != Bar"); + regular(FilterComparison.neq(FOO, BAR), MatchFilter.class, "Foo not in [Bar]"); inverse(FilterComparison.neq(FOO, V42), MatchFilter.class, "Foo in [42]"); inverse(FilterComparison.neq(V42, FOO), MatchFilter.class, "Foo in [42]"); inverse(FilterComparison.neq(FOO, HELLO), MatchFilter.class, "Foo in [Hello]"); inverse(FilterComparison.neq(HELLO, FOO), MatchFilter.class, "Foo in [Hello]"); - inverse(FilterComparison.neq(FOO, BAR), ConditionFilter.class, "Foo == Bar"); + inverse(FilterComparison.neq(FOO, BAR), MatchFilter.class, "Foo in [Bar]"); } public void testGt() { - regular(FilterComparison.gt(FOO, V42), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than 42)"); - regular(FilterComparison.gt(V42, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than 42)"); - regular(FilterComparison.gt(FOO, HELLO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than \"Hello\")"); - regular(FilterComparison.gt(HELLO, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than \"Hello\")"); - regular(FilterComparison.gt(FOO, BAR), ConditionFilter.class, "Foo > Bar"); - - inverse(FilterComparison.gt(FOO, V42), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than or equal to 42)"); - inverse(FilterComparison.gt(V42, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than or equal to 42)"); - inverse(FilterComparison.gt(FOO, HELLO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than or equal to \"Hello\")"); - inverse(FilterComparison.gt(HELLO, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than or equal to \"Hello\")"); - inverse(FilterComparison.gt(FOO, BAR), ConditionFilter.class, "Foo <= Bar"); + regular(FilterComparison.gt(FOO, V42), RangeFilter.class, + "RangeFilter(Foo greater than 42)"); + regular(FilterComparison.gt(V42, FOO), RangeFilter.class, + "RangeFilter(Foo less than 42)"); + regular(FilterComparison.gt(FOO, HELLO), RangeFilter.class, + "RangeFilter(Foo greater than \"Hello\")"); + regular(FilterComparison.gt(HELLO, FOO), RangeFilter.class, + "RangeFilter(Foo less than \"Hello\")"); + regular(FilterComparison.gt(FOO, BAR), RangeFilter.class, "RangeFilter(Foo greater than Bar)"); + + inverse(FilterComparison.gt(FOO, V42), RangeFilter.class, + "RangeFilter(Foo less than or equal to 42)"); + inverse(FilterComparison.gt(V42, FOO), RangeFilter.class, + "RangeFilter(Foo greater than or equal to 42)"); + inverse(FilterComparison.gt(FOO, HELLO), RangeFilter.class, + "RangeFilter(Foo less than or equal to \"Hello\")"); + inverse(FilterComparison.gt(HELLO, FOO), RangeFilter.class, + "RangeFilter(Foo greater than or equal to \"Hello\")"); + inverse(FilterComparison.gt(FOO, BAR), RangeFilter.class, "RangeFilter(Foo less than or equal to Bar)"); } public void testGte() { - regular(FilterComparison.geq(FOO, V42), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than or equal to 42)"); - regular(FilterComparison.geq(V42, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than or equal to 42)"); - regular(FilterComparison.geq(FOO, HELLO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than or equal to \"Hello\")"); - regular(FilterComparison.geq(HELLO, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than or equal to \"Hello\")"); - regular(FilterComparison.geq(FOO, BAR), ConditionFilter.class, "Foo >= Bar"); - - inverse(FilterComparison.geq(FOO, V42), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than 42)"); - inverse(FilterComparison.geq(V42, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than 42)"); - inverse(FilterComparison.geq(FOO, HELLO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than \"Hello\")"); - inverse(FilterComparison.geq(HELLO, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than \"Hello\")"); - inverse(FilterComparison.geq(FOO, BAR), ConditionFilter.class, "Foo < Bar"); + regular(FilterComparison.geq(FOO, V42), RangeFilter.class, + "RangeFilter(Foo greater than or equal to 42)"); + regular(FilterComparison.geq(V42, FOO), RangeFilter.class, + "RangeFilter(Foo less than or equal to 42)"); + regular(FilterComparison.geq(FOO, HELLO), RangeFilter.class, + "RangeFilter(Foo greater than or equal to \"Hello\")"); + regular(FilterComparison.geq(HELLO, FOO), RangeFilter.class, + "RangeFilter(Foo less than or equal to \"Hello\")"); + regular(FilterComparison.geq(FOO, BAR), RangeFilter.class, "RangeFilter(Foo greater than or equal to Bar)"); + + inverse(FilterComparison.geq(FOO, V42), RangeFilter.class, + "RangeFilter(Foo less than 42)"); + inverse(FilterComparison.geq(V42, FOO), RangeFilter.class, + "RangeFilter(Foo greater than 42)"); + inverse(FilterComparison.geq(FOO, HELLO), RangeFilter.class, + "RangeFilter(Foo less than \"Hello\")"); + inverse(FilterComparison.geq(HELLO, FOO), RangeFilter.class, + "RangeFilter(Foo greater than \"Hello\")"); + inverse(FilterComparison.geq(FOO, BAR), RangeFilter.class, "RangeFilter(Foo less than Bar)"); } public void testLt() { - regular(FilterComparison.lt(FOO, V42), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than 42)"); - regular(FilterComparison.lt(V42, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than 42)"); - regular(FilterComparison.lt(FOO, HELLO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than \"Hello\")"); - regular(FilterComparison.lt(HELLO, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than \"Hello\")"); - regular(FilterComparison.lt(FOO, BAR), ConditionFilter.class, "Foo < Bar"); - - inverse(FilterComparison.lt(FOO, V42), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than or equal to 42)"); - inverse(FilterComparison.lt(V42, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than or equal to 42)"); - inverse(FilterComparison.lt(FOO, HELLO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than or equal to \"Hello\")"); - inverse(FilterComparison.lt(HELLO, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than or equal to \"Hello\")"); - inverse(FilterComparison.lt(FOO, BAR), ConditionFilter.class, "Foo >= Bar"); + regular(FilterComparison.lt(FOO, V42), RangeFilter.class, + "RangeFilter(Foo less than 42)"); + regular(FilterComparison.lt(V42, FOO), RangeFilter.class, + "RangeFilter(Foo greater than 42)"); + regular(FilterComparison.lt(FOO, HELLO), RangeFilter.class, + "RangeFilter(Foo less than \"Hello\")"); + regular(FilterComparison.lt(HELLO, FOO), RangeFilter.class, + "RangeFilter(Foo greater than \"Hello\")"); + regular(FilterComparison.lt(FOO, BAR), RangeFilter.class, "RangeFilter(Foo less than Bar)"); + + inverse(FilterComparison.lt(FOO, V42), RangeFilter.class, + "RangeFilter(Foo greater than or equal to 42)"); + inverse(FilterComparison.lt(V42, FOO), RangeFilter.class, + "RangeFilter(Foo less than or equal to 42)"); + inverse(FilterComparison.lt(FOO, HELLO), RangeFilter.class, + "RangeFilter(Foo greater than or equal to \"Hello\")"); + inverse(FilterComparison.lt(HELLO, FOO), RangeFilter.class, + "RangeFilter(Foo less than or equal to \"Hello\")"); + inverse(FilterComparison.lt(FOO, BAR), RangeFilter.class, "RangeFilter(Foo greater than or equal to Bar)"); } public void testLte() { - regular(FilterComparison.leq(FOO, V42), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than or equal to 42)"); - regular(FilterComparison.leq(V42, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than or equal to 42)"); - regular(FilterComparison.leq(FOO, HELLO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than or equal to \"Hello\")"); - regular(FilterComparison.leq(HELLO, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than or equal to \"Hello\")"); - regular(FilterComparison.leq(FOO, BAR), ConditionFilter.class, "Foo <= Bar"); - - inverse(FilterComparison.leq(FOO, V42), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than 42)"); - inverse(FilterComparison.leq(V42, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than 42)"); - inverse(FilterComparison.leq(FOO, HELLO), RangeConditionFilter.class, - "RangeConditionFilter(Foo greater than \"Hello\")"); - inverse(FilterComparison.leq(HELLO, FOO), RangeConditionFilter.class, - "RangeConditionFilter(Foo less than \"Hello\")"); - inverse(FilterComparison.leq(FOO, BAR), ConditionFilter.class, "Foo > Bar"); + regular(FilterComparison.leq(FOO, V42), RangeFilter.class, + "RangeFilter(Foo less than or equal to 42)"); + regular(FilterComparison.leq(V42, FOO), RangeFilter.class, + "RangeFilter(Foo greater than or equal to 42)"); + regular(FilterComparison.leq(FOO, HELLO), RangeFilter.class, + "RangeFilter(Foo less than or equal to \"Hello\")"); + regular(FilterComparison.leq(HELLO, FOO), RangeFilter.class, + "RangeFilter(Foo greater than or equal to \"Hello\")"); + regular(FilterComparison.leq(FOO, BAR), RangeFilter.class, "RangeFilter(Foo less than or equal to Bar)"); + + inverse(FilterComparison.leq(FOO, V42), RangeFilter.class, + "RangeFilter(Foo greater than 42)"); + inverse(FilterComparison.leq(V42, FOO), RangeFilter.class, + "RangeFilter(Foo less than 42)"); + inverse(FilterComparison.leq(FOO, HELLO), RangeFilter.class, + "RangeFilter(Foo greater than \"Hello\")"); + inverse(FilterComparison.leq(HELLO, FOO), RangeFilter.class, + "RangeFilter(Foo less than \"Hello\")"); + inverse(FilterComparison.leq(FOO, BAR), RangeFilter.class, "RangeFilter(Foo greater than Bar)"); } public void testFunction() { @@ -303,8 +303,8 @@ public void testInLiteralsDifferentTypes() { public void testInSingleNotLiteral() { final FilterIn in = FilterIn.of(FOO, BAR); - regular(in, ConditionFilter.class, "Foo == Bar"); - inverse(in, ConditionFilter.class, "Foo != Bar"); + regular(in, MatchFilter.class, "Foo in [Bar]"); + inverse(in, MatchFilter.class, "Foo not in [Bar]"); } diff --git a/engine/time/src/main/java/io/deephaven/time/DateTimeUtils.java b/engine/time/src/main/java/io/deephaven/time/DateTimeUtils.java index ce572dd30fc..73e4cbbe535 100644 --- a/engine/time/src/main/java/io/deephaven/time/DateTimeUtils.java +++ b/engine/time/src/main/java/io/deephaven/time/DateTimeUtils.java @@ -81,6 +81,13 @@ public class DateTimeUtils { private static final Pattern DATE_TZ_PATTERN = Pattern.compile( "(?[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])(?[tT]?) (?[a-zA-Z_/]+)"); + /** + * Matches dates without time zones. + */ + private static final Pattern LOCAL_DATE_PATTERN = Pattern.compile( + "(?[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])(?[tT]?)"); + + /** * Matches time durations. */ @@ -5003,6 +5010,65 @@ public static Instant parseInstantQuiet(@Nullable final String s) { } } + /** + * Parses the string argument as a {@link LocalDateTime}. + *

    + * Date time strings are formatted according to the ISO 8601 date time format + * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS]} and others. + * + * @param s date time string + * @return a {@link LocalDateTime} represented by the input string + * @throws DateTimeParseException if the string cannot be parsed + */ + @ScriptApi + @NotNull + public static LocalDateTime parseLocalDateTime(@NotNull final String s) { + // noinspection ConstantConditions + if (s == null) { + throw new DateTimeParseException("Cannot parse local date time (null): " + s); + } + + try { + return LocalDateTime.parse(s); + } catch (java.time.format.DateTimeParseException e) { + // ignore + } + + try { + final Matcher dtMatcher = LOCAL_DATE_PATTERN.matcher(s); + if (dtMatcher.matches()) { + final String dateString = dtMatcher.group("date"); + return LocalDate.parse(dateString, FORMATTER_ISO_LOCAL_DATE).atTime(LocalTime.of(0, 0)); + } + return LocalDateTime.parse(s, FORMATTER_ISO_LOCAL_DATE_TIME); + } catch (Exception ex) { + throw new DateTimeParseException("Cannot parse local date time: " + s, ex); + } + } + + /** + * Parses the string argument as a {@link LocalDateTime}. + *

    + * Date time strings are formatted according to the ISO 8601 date time format + * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS]} and others. + * + * @param s date time string + * @return a {@link LocalDateTime} represented by the input string, or {@code null} if the string can not be parsed + */ + @ScriptApi + @Nullable + public static LocalDateTime parseLocalDateTimeQuiet(@Nullable final String s) { + if (s == null || s.length() <= 1) { + return null; + } + + try { + return parseLocalDateTime(s); + } catch (Exception e) { + return null; + } + } + /** * Parses the string argument as a {@link ZonedDateTime}. *

    diff --git a/engine/time/src/test/java/io/deephaven/time/TestDateTimeUtils.java b/engine/time/src/test/java/io/deephaven/time/TestDateTimeUtils.java index 8515f0f94bb..7dcad0ad771 100644 --- a/engine/time/src/test/java/io/deephaven/time/TestDateTimeUtils.java +++ b/engine/time/src/test/java/io/deephaven/time/TestDateTimeUtils.java @@ -566,6 +566,138 @@ public void testParseInstantQuiet() { TestCase.assertEquals(dt1s, DateTimeUtils.parseInstantQuiet(Long.toString(seconds))); } + public void testParseLocalDateTime() { + final String[] roots = { + "2010-01-01T12:11", + "2010-01-01T12:00:02", + "2010-01-01T12:00:00.1", + "2010-01-01T12:00:00.123", + "2010-01-01T12:00:00.123", + "2010-01-01T12:00:00.123456789", + }; + + for (String root : roots) { + final LocalDateTime ldt = LocalDateTime.parse(root); + TestCase.assertEquals("LocalDateTime string: " + root, ldt, DateTimeUtils.parseLocalDateTime(root)); + } + + final String[] uglyRoots = { + "2023-04-30", + "2023-04-30T", + "2023-04-30t", + "2023-04-30T9:30:00", + "2023-4-3T9:3:6", + "2023-4-3T9:3", + "2023-4-3T9:3:6.1", + "2023-4-3T9:3:6.123", + "2023-4-3T9:3:6.123456789", + }; + + final LocalDateTime[] uglyLDTs = { + LocalDateTime.of(2023, 4, 30, 0, 0), + LocalDateTime.of(2023, 4, 30, 0, 0), + LocalDateTime.of(2023, 4, 30, 0, 0), + LocalDateTime.of(2023, 4, 30, 9, 30, 0), + LocalDateTime.of(2023, 4, 3, 9, 3, 6), + LocalDateTime.of(2023, 4, 3, 9, 3, 0), + LocalDateTime.of(2023, 4, 3, 9, 3, 6, 100_000_000), + LocalDateTime.of(2023, 4, 3, 9, 3, 6, 123_000_000), + LocalDateTime.of(2023, 4, 3, 9, 3, 6, 123456789), + }; + + for (int i = 0; i < uglyRoots.length; i++) { + final String root = uglyRoots[i]; + final LocalDateTime ldt = uglyLDTs[i]; + TestCase.assertEquals("LocalDateTime string: " + root, ldt, DateTimeUtils.parseLocalDateTime(root)); + } + + try { + DateTimeUtils.parseLocalDateTime("JUNK"); + TestCase.fail("Should throw an exception"); + } catch (Exception ex) { + // pass + } + + try { + DateTimeUtils.parseLocalDateTime("2010-01-01JUNK12:11"); + TestCase.fail("Should throw an exception"); + } catch (Exception ex) { + // pass + } + + try { + DateTimeUtils.parseLocalDateTime("2010-01-01T12:11 JUNK"); + TestCase.fail("Should throw an exception"); + } catch (Exception ex) { + // pass + } + + try { + // noinspection ConstantConditions + DateTimeUtils.parseLocalDateTime(null); + TestCase.fail("Should throw an exception"); + } catch (Exception ex) { + // pass + } + + final String iso8601 = "2022-04-26T00:30:31.087360"; + assertEquals(LocalDateTime.parse(iso8601), DateTimeUtils.parseLocalDateTime(iso8601)); + } + + public void testParseLocalDateTimeQuiet() { + final String[] roots = { + "2010-01-01T12:11", + "2010-01-01T12:00:02", + "2010-01-01T12:00:00.1", + "2010-01-01T12:00:00.123", + "2010-01-01T12:00:00.123", + "2010-01-01T12:00:00.123456789", + }; + + for (String root : roots) { + final LocalDateTime ldt = LocalDateTime.parse(root); + TestCase.assertEquals("LocalDateTime string: " + root, ldt, DateTimeUtils.parseLocalDateTime(root)); + } + + final String[] uglyRoots = { + "2023-04-30", + "2023-04-30T", + "2023-04-30t", + "2023-04-30T9:30:00", + "2023-4-3T9:3:6", + "2023-4-3T9:3", + "2023-4-3T9:3:6.1", + "2023-4-3T9:3:6.123", + "2023-4-3T9:3:6.123456789", + }; + + final LocalDateTime[] uglyLDTs = { + LocalDateTime.of(2023, 4, 30, 0, 0), + LocalDateTime.of(2023, 4, 30, 0, 0), + LocalDateTime.of(2023, 4, 30, 0, 0), + LocalDateTime.of(2023, 4, 30, 9, 30, 0), + LocalDateTime.of(2023, 4, 3, 9, 3, 6), + LocalDateTime.of(2023, 4, 3, 9, 3, 0), + LocalDateTime.of(2023, 4, 3, 9, 3, 6, 100_000_000), + LocalDateTime.of(2023, 4, 3, 9, 3, 6, 123_000_000), + LocalDateTime.of(2023, 4, 3, 9, 3, 6, 123456789), + }; + + for (int i = 0; i < uglyRoots.length; i++) { + final String root = uglyRoots[i]; + final LocalDateTime ldt = uglyLDTs[i]; + TestCase.assertEquals("LocalDateTime string: " + root, ldt, DateTimeUtils.parseLocalDateTime(root)); + } + + TestCase.assertNull(DateTimeUtils.parseLocalDateTimeQuiet("JUNK")); + TestCase.assertNull(DateTimeUtils.parseLocalDateTimeQuiet("2010-01-01JUNK12:11")); + TestCase.assertNull(DateTimeUtils.parseLocalDateTimeQuiet("2010-01-01T12:11 JUNK")); + TestCase.assertNull(DateTimeUtils.parseLocalDateTimeQuiet(null)); + + final String iso8601 = "2022-04-26T00:30:31.087360"; + assertEquals(LocalDateTime.parse(iso8601), DateTimeUtils.parseLocalDateTime(iso8601)); + } + public void testParseZonedDateTime() { final String[] tzs = { "NY", diff --git a/server/src/main/java/io/deephaven/server/table/ops/filter/FilterFactory.java b/server/src/main/java/io/deephaven/server/table/ops/filter/FilterFactory.java index 7b5edc29c9a..4ad44c629ca 100644 --- a/server/src/main/java/io/deephaven/server/table/ops/filter/FilterFactory.java +++ b/server/src/main/java/io/deephaven/server/table/ops/filter/FilterFactory.java @@ -11,7 +11,7 @@ import io.deephaven.engine.table.impl.select.DisjunctiveFilter; import io.deephaven.engine.table.impl.select.FormulaParserConfiguration; import io.deephaven.engine.table.impl.select.MatchFilter; -import io.deephaven.engine.table.impl.select.RangeConditionFilter; +import io.deephaven.engine.table.impl.select.RangeFilter; import io.deephaven.engine.table.impl.select.WhereFilter; import io.deephaven.engine.table.impl.select.WhereFilterFactory; import io.deephaven.engine.table.impl.select.WhereNoneFilter; @@ -145,7 +145,7 @@ private WhereFilter generateNumericConditionFilter(CompareCondition.CompareOpera default: throw new IllegalStateException("Range filter can't handle literal type " + value.getValueCase()); } - return new RangeConditionFilter(columName, rangeCondition(operation, invert), valueString, null, + return new RangeFilter(columName, rangeCondition(operation, invert), valueString, null, FormulaParserConfiguration.parser); }