blob: 71b477d16c37cb1c9e7f59c91b1d4432354fdee3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.druid.query;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Ordering;
import nl.jqno.equalsverifier.EqualsVerifier;
import nl.jqno.equalsverifier.Warning;
import org.apache.druid.java.util.common.HumanReadableBytes;
import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.java.util.common.granularity.Granularity;
import org.apache.druid.query.aggregation.CountAggregatorFactory;
import org.apache.druid.query.filter.DimFilter;
import org.apache.druid.query.spec.QuerySegmentSpec;
import org.apache.druid.segment.DimensionHandlerUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.junit.Test;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class QueryContextTest
{
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
@Test
public void testEquals()
{
EqualsVerifier.configure()
.suppress(Warning.NONFINAL_FIELDS, Warning.ALL_FIELDS_SHOULD_BE_USED)
.usingGetClass()
.forClass(QueryContext.class)
.withNonnullFields("context")
.verify();
}
/**
* Verify that a context with an null map is the same as a context with
* an empty map.
*/
@Test
public void testEmptyContext()
{
{
final QueryContext context = new QueryContext(null);
assertEquals(ImmutableMap.of(), context.asMap());
}
{
final QueryContext context = new QueryContext(new HashMap<>());
assertEquals(ImmutableMap.of(), context.asMap());
}
{
final QueryContext context = QueryContext.of(null);
assertEquals(ImmutableMap.of(), context.asMap());
}
{
final QueryContext context = QueryContext.of(new HashMap<>());
assertEquals(ImmutableMap.of(), context.asMap());
}
{
final QueryContext context = QueryContext.empty();
assertEquals(ImmutableMap.of(), context.asMap());
}
}
@Test
public void testIsEmpty()
{
assertTrue(QueryContext.empty().isEmpty());
assertFalse(QueryContext.of(ImmutableMap.of("k", "v")).isEmpty());
}
@Test
public void testGetString()
{
final QueryContext context = QueryContext.of(
ImmutableMap.of("key", "val",
"key2", 2)
);
assertEquals("val", context.get("key"));
assertEquals("val", context.getString("key"));
assertNull(context.getString("non-exist"));
assertEquals("foo", context.getString("non-exist", "foo"));
assertThrows(BadQueryContextException.class, () -> context.getString("key2"));
}
@Test
public void testGetBoolean()
{
final QueryContext context = QueryContext.of(
ImmutableMap.of(
"key1", "true",
"key2", true
)
);
assertTrue(context.getBoolean("key1", false));
assertTrue(context.getBoolean("key2", false));
assertTrue(context.getBoolean("key1"));
assertFalse(context.getBoolean("non-exist", false));
assertNull(context.getBoolean("non-exist"));
}
@Test
public void testGetInt()
{
final QueryContext context = QueryContext.of(
ImmutableMap.of(
"key1", "100",
"key2", 100,
"key3", "abc"
)
);
assertEquals(100, context.getInt("key1", 0));
assertEquals(100, context.getInt("key2", 0));
assertEquals(0, context.getInt("non-exist", 0));
assertThrows(BadQueryContextException.class, () -> context.getInt("key3", 5));
}
@Test
public void testGetLong()
{
final QueryContext context = QueryContext.of(
ImmutableMap.of(
"key1", "100",
"key2", 100,
"key3", "abc"
)
);
assertEquals(100L, context.getLong("key1", 0));
assertEquals(100L, context.getLong("key2", 0));
assertEquals(0L, context.getLong("non-exist", 0));
assertThrows(BadQueryContextException.class, () -> context.getLong("key3", 5));
}
/**
* Tests the several ways that Druid code parses context strings into Long
* values. The desired behavior is that "x" is parsed exactly the same as Jackson
* would parse x (where x is a valid number.) The context methods must emulate
* Jackson. The dimension utility method is included because some code used that
* for long parsing, and we must maintain backward compatibility.
* <p>
* The exceptions in the {@code assertThrows} are not critical: the key thing is
* that we're documenting what works and what doesn't. If an exception changes,
* just update the tests. If something no longer throws an exception, we'll want
* to verify that we support the new use case consistently in all three paths.
*/
@Test
public void testGetLongCompatibility() throws JsonProcessingException
{
{
String value = null;
// Only the context methods allow {"foo": null} to be parsed as a null Long.
assertNull(getContextLong(value));
// Nulls not legal on this path.
assertThrows(NullPointerException.class, () -> getDimensionLong(value));
// Nulls not legal on this path.
assertThrows(IllegalArgumentException.class, () -> getJsonLong(value));
}
{
String value = "";
// Blank string not legal on this path.
assertThrows(BadQueryContextException.class, () -> getContextLong(value));
assertNull(getDimensionLong(value));
// Blank string not allowed where a value is expected.
assertThrows(MismatchedInputException.class, () -> getJsonLong(value));
}
{
String value = "0";
assertEquals(0L, (long) getContextLong(value));
assertEquals(0L, (long) getDimensionLong(value));
assertEquals(0L, (long) getJsonLong(value));
}
{
String value = "+1";
assertEquals(1L, (long) getContextLong(value));
assertEquals(1L, (long) getDimensionLong(value));
assertThrows(JsonParseException.class, () -> getJsonLong(value));
}
{
String value = "-1";
assertEquals(-1L, (long) getContextLong(value));
assertEquals(-1L, (long) getDimensionLong(value));
assertEquals(-1L, (long) getJsonLong(value));
}
{
// Hexadecimal numbers are not supported in JSON. Druid also does not support
// them in strings.
String value = "0xabcd";
assertThrows(BadQueryContextException.class, () -> getContextLong(value));
// The dimension utils have a funny way of handling hex: they return null
assertNull(getDimensionLong(value));
assertThrows(JsonParseException.class, () -> getJsonLong(value));
}
{
// Leading zeros supported by Druid parsing, but not by JSON.
String value = "05";
assertEquals(5L, (long) getContextLong(value));
assertEquals(5L, (long) getDimensionLong(value));
assertThrows(JsonParseException.class, () -> getJsonLong(value));
}
{
// The dimension utils allow a float where a long is expected.
// Jackson can do this conversion. This test verifies that the context
// functions can handle the same conversion.
String value = "10.00";
assertEquals(10L, (long) getContextLong(value));
assertEquals(10L, (long) getDimensionLong(value));
assertEquals(10L, (long) getJsonLong(value));
}
{
// None of the conversion methods allow a (thousands) separator. The comma
// would be ambiguous in JSON. Java allows the underscore, but JSON does
// not support this syntax, and neither does Druid's string-to-long conversion.
String value = "1_234";
assertThrows(BadQueryContextException.class, () -> getContextLong(value));
assertNull(getDimensionLong(value));
assertThrows(JsonParseException.class, () -> getJsonLong(value));
}
}
private static Long getContextLong(String value)
{
return QueryContexts.getAsLong("dummy", value);
}
private static Long getJsonLong(String value) throws JsonProcessingException
{
return JSON_MAPPER.readValue(value, Long.class);
}
private static Long getDimensionLong(String value)
{
return DimensionHandlerUtils.getExactLongFromDecimalString(value);
}
@Test
public void testGetFloat()
{
final QueryContext context = QueryContext.of(
ImmutableMap.of(
"f1", "500",
"f2", 500,
"f3", 500.1,
"f4", "ab"
)
);
assertEquals(0, Float.compare(500, context.getFloat("f1", 100)));
assertEquals(0, Float.compare(500, context.getFloat("f2", 100)));
assertEquals(0, Float.compare(500.1f, context.getFloat("f3", 100)));
assertThrows(BadQueryContextException.class, () -> context.getFloat("f4", 5));
}
@Test
public void testGetHumanReadableBytes()
{
final QueryContext context = new QueryContext(
ImmutableMap.<String, Object>builder()
.put("m1", 500_000_000)
.put("m2", "500M")
.put("m3", "500Mi")
.put("m4", "500MiB")
.put("m5", "500000000")
.put("m6", "abc")
.build()
);
assertEquals(500_000_000, context.getHumanReadableBytes("m1", HumanReadableBytes.ZERO).getBytes());
assertEquals(500_000_000, context.getHumanReadableBytes("m2", HumanReadableBytes.ZERO).getBytes());
assertEquals(500 * 1024 * 1024L, context.getHumanReadableBytes("m3", HumanReadableBytes.ZERO).getBytes());
assertEquals(500 * 1024 * 1024L, context.getHumanReadableBytes("m4", HumanReadableBytes.ZERO).getBytes());
assertEquals(500_000_000, context.getHumanReadableBytes("m5", HumanReadableBytes.ZERO).getBytes());
assertThrows(BadQueryContextException.class, () -> context.getHumanReadableBytes("m6", HumanReadableBytes.ZERO));
}
@Test
public void testGetMaxSubqueryBytes()
{
final QueryContext context1 = new QueryContext(
ImmutableMap.of(QueryContexts.MAX_SUBQUERY_BYTES_KEY, 500_000_000)
);
assertEquals("500000000", context1.getMaxSubqueryMemoryBytes(null));
final QueryContext context2 = new QueryContext(
ImmutableMap.of(QueryContexts.MAX_SUBQUERY_BYTES_KEY, "auto")
);
assertEquals("auto", context2.getMaxSubqueryMemoryBytes(null));
final QueryContext context3 = new QueryContext(ImmutableMap.of());
assertEquals("disabled", context3.getMaxSubqueryMemoryBytes("disabled"));
}
@Test
public void testDefaultEnableQueryDebugging()
{
assertFalse(QueryContext.empty().isDebug());
assertTrue(QueryContext.of(ImmutableMap.of(QueryContexts.ENABLE_DEBUG, true)).isDebug());
}
// This test is a bit silly. It is retained because another test uses the
// LegacyContextQuery test.
@Test
public void testLegacyReturnsLegacy()
{
Map<String, Object> context = ImmutableMap.of("foo", "bar");
Query<?> legacy = new LegacyContextQuery(context);
assertEquals(context, legacy.getContext());
}
@Test
public void testNonLegacyIsNotLegacyContext()
{
Query<?> timeseries = Druids.newTimeseriesQueryBuilder()
.dataSource("test")
.intervals("2015-01-02/2015-01-03")
.granularity(Granularities.DAY)
.aggregators(Collections.singletonList(new CountAggregatorFactory("theCount")))
.context(ImmutableMap.of("foo", "bar"))
.build();
assertNotNull(timeseries.getContext());
}
public static class LegacyContextQuery implements Query<Integer>
{
private final Map<String, Object> context;
public LegacyContextQuery(Map<String, Object> context)
{
this.context = context;
}
@Override
public DataSource getDataSource()
{
return new TableDataSource("fake");
}
@Override
public boolean hasFilters()
{
return false;
}
@Override
public DimFilter getFilter()
{
return null;
}
@Override
public String getType()
{
return "legacy-context-query";
}
@Override
public QueryRunner<Integer> getRunner(QuerySegmentWalker walker)
{
return new NoopQueryRunner<>();
}
@Override
public List<Interval> getIntervals()
{
return Collections.singletonList(Intervals.ETERNITY);
}
@Override
public Duration getDuration()
{
return getIntervals().get(0).toDuration();
}
@Override
public Granularity getGranularity()
{
return Granularities.ALL;
}
@Override
public DateTimeZone getTimezone()
{
return DateTimeZone.UTC;
}
@Override
public Map<String, Object> getContext()
{
return context;
}
@Override
public boolean isDescending()
{
return false;
}
@Override
public Ordering<Integer> getResultOrdering()
{
return Ordering.natural();
}
@Override
public Query<Integer> withQuerySegmentSpec(QuerySegmentSpec spec)
{
return new LegacyContextQuery(context);
}
@Override
public Query<Integer> withId(String id)
{
context.put(BaseQuery.QUERY_ID, id);
return this;
}
@Nullable
@Override
public String getId()
{
return (String) context.get(BaseQuery.QUERY_ID);
}
@Override
public Query<Integer> withSubQueryId(String subQueryId)
{
context.put(BaseQuery.SUB_QUERY_ID, subQueryId);
return this;
}
@Nullable
@Override
public String getSubQueryId()
{
return (String) context.get(BaseQuery.SUB_QUERY_ID);
}
@Override
public Query<Integer> withDataSource(DataSource dataSource)
{
return this;
}
@Override
public Query<Integer> withOverriddenContext(Map<String, Object> contextOverride)
{
return new LegacyContextQuery(contextOverride);
}
}
}