diff --git a/docs/content/querying/having.md b/docs/content/querying/having.md index bf5500eb1bf..c3f21135554 100644 --- a/docs/content/querying/having.md +++ b/docs/content/querying/having.md @@ -68,6 +68,24 @@ The grammar for a `greaterThan` filter is as follows: This is the equivalent of `HAVING < `. + + +### Dimension Selector Filter + +#### dimSelector + +The dimSelector filter will match rows with dimension values equal to the specified value. +The grammar for a `dimSelector` filter is as follows: + +```json +{ + "type": "dimSelector", + "dimension": "", + "value": +} +``` + + ### Logical expression filters #### AND diff --git a/processing/src/main/java/io/druid/query/groupby/having/DimensionSelectorHavingSpec.java b/processing/src/main/java/io/druid/query/groupby/having/DimensionSelectorHavingSpec.java new file mode 100644 index 00000000000..57cb5753ab7 --- /dev/null +++ b/processing/src/main/java/io/druid/query/groupby/having/DimensionSelectorHavingSpec.java @@ -0,0 +1,140 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets 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 io.druid.query.groupby.having; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.metamx.common.StringUtils; +import io.druid.data.input.Row; + +import java.nio.ByteBuffer; +import java.util.List; + +public class DimensionSelectorHavingSpec implements HavingSpec +{ + private static final byte CACHE_KEY = (byte) 0x8; + private static final byte STRING_SEPARATOR = (byte) 0xFF; + private final String dimension; + private final String value; + + @JsonCreator + public DimensionSelectorHavingSpec( + @JsonProperty("dimension") String dimName, + @JsonProperty("value") String value + ) + { + this.dimension = Preconditions.checkNotNull(dimName, "Must have attribute 'dimension'"); + this.value = value; + } + + @JsonProperty("value") + public String getValue() + { + return this.value; + } + + @JsonProperty("dimension") + public String getDimension() + { + return this.dimension; + } + + public boolean eval(Row row) + { + List dimRowValList = row.getDimension(this.dimension); + if (dimRowValList == null || dimRowValList.isEmpty()) { + return Strings.isNullOrEmpty(value); + } + + for (String rowVal : dimRowValList) { + if (this.value != null && this.value.equals(rowVal)) { + return true; + } + if (rowVal == null || rowVal.isEmpty()) { + return Strings.isNullOrEmpty(value); + } + } + + return false; + } + + public byte[] getCacheKey() + { + byte[] dimBytes = StringUtils.toUtf8(this.dimension); + byte[] valBytes = StringUtils.toUtf8(this.value); + return ByteBuffer.allocate(2 + dimBytes.length + valBytes.length) + .put(CACHE_KEY) + .put(dimBytes) + .put(STRING_SEPARATOR) + .put(valBytes) + .array(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DimensionSelectorHavingSpec that = (DimensionSelectorHavingSpec) o; + boolean valEquals = false; + boolean dimEquals = false; + + if (value != null && that.value != null) { + valEquals = value.equals(that.value); + } else if (value == null && that.value == null) { + valEquals = true; + } + + if (dimension != null && that.dimension != null) { + dimEquals = dimension.equals(that.dimension); + } else if (dimension == null && that.dimension == null) { + dimEquals = true; + } + + return (valEquals && dimEquals); + } + + @Override + public int hashCode() + { + int result = dimension != null ? dimension.hashCode() : 0; + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("DimensionSelectorHavingSpec"); + sb.append("{dimension='").append(this.dimension).append('\''); + sb.append(", value='").append(this.value).append('\''); + sb.append('}'); + return sb.toString(); + } + +} diff --git a/processing/src/main/java/io/druid/query/groupby/having/HavingSpec.java b/processing/src/main/java/io/druid/query/groupby/having/HavingSpec.java index ee3d92a246a..1f51acd8413 100644 --- a/processing/src/main/java/io/druid/query/groupby/having/HavingSpec.java +++ b/processing/src/main/java/io/druid/query/groupby/having/HavingSpec.java @@ -22,7 +22,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.druid.data.input.Row; /** - * A "having" clause that filters aggregated value. This is similar to SQL's "having" + * A "having" clause that filters aggregated/dimension value. This is similar to SQL's "having" * clause. */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = AlwaysHavingSpec.class) @@ -32,7 +32,8 @@ import io.druid.data.input.Row; @JsonSubTypes.Type(name = "not", value = NotHavingSpec.class), @JsonSubTypes.Type(name = "greaterThan", value = GreaterThanHavingSpec.class), @JsonSubTypes.Type(name = "lessThan", value = LessThanHavingSpec.class), - @JsonSubTypes.Type(name = "equalTo", value = EqualToHavingSpec.class) + @JsonSubTypes.Type(name = "equalTo", value = EqualToHavingSpec.class), + @JsonSubTypes.Type(name = "dimSelector", value = DimensionSelectorHavingSpec.class) }) public interface HavingSpec { diff --git a/processing/src/test/java/io/druid/query/groupby/having/DimensionSelectorHavingSpecTest.java b/processing/src/test/java/io/druid/query/groupby/having/DimensionSelectorHavingSpecTest.java new file mode 100644 index 00000000000..57c280a05c7 --- /dev/null +++ b/processing/src/test/java/io/druid/query/groupby/having/DimensionSelectorHavingSpecTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets 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 io.druid.query.groupby.having; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.druid.data.input.MapBasedRow; +import io.druid.data.input.Row; +import io.druid.jackson.DefaultObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + + +public class DimensionSelectorHavingSpecTest +{ + + private static final byte CACHE_KEY = (byte) 0x8; + private static final byte STRING_SEPARATOR = (byte) 0xFF; + + private Row getTestRow(Object dimensionValue) + { + return new MapBasedRow(0, ImmutableMap.of("dimension", dimensionValue)); + } + + @Test + public void testDimSelectorHavingClauseSerde() throws Exception + { + HavingSpec dimHavingSpec = new DimensionSelectorHavingSpec("dim", "v"); + + Map dimSelectMap = ImmutableMap.of( + "type", "dimSelector", + "dimension", "dim", + "value", "v" + ); + + ObjectMapper mapper = new DefaultObjectMapper(); + assertEquals(dimHavingSpec, mapper.convertValue(dimSelectMap, DimensionSelectorHavingSpec.class)); + + } + + @Test + public void testEquals() throws Exception + { + HavingSpec dimHavingSpec1 = new DimensionSelectorHavingSpec("dim", "v"); + HavingSpec dimHavingSpec2 = new DimensionSelectorHavingSpec("dim", "v"); + HavingSpec dimHavingSpec3 = new DimensionSelectorHavingSpec("dim1", "v"); + HavingSpec dimHavingSpec4 = new DimensionSelectorHavingSpec("dim2", "v"); + HavingSpec dimHavingSpec5 = new DimensionSelectorHavingSpec("dim", "v1"); + HavingSpec dimHavingSpec6 = new DimensionSelectorHavingSpec("dim", "v2"); + HavingSpec dimHavingSpec7 = new DimensionSelectorHavingSpec("dim", null); + HavingSpec dimHavingSpec8 = new DimensionSelectorHavingSpec("dim", null); + HavingSpec dimHavingSpec9 = new DimensionSelectorHavingSpec("dim1", null); + HavingSpec dimHavingSpec10 = new DimensionSelectorHavingSpec("dim2", null); + HavingSpec dimHavingSpec11 = new DimensionSelectorHavingSpec("dim1", "v"); + HavingSpec dimHavingSpec12 = new DimensionSelectorHavingSpec("dim2", null); + + assertEquals(dimHavingSpec1, dimHavingSpec2); + assertNotEquals(dimHavingSpec3, dimHavingSpec4); + assertNotEquals(dimHavingSpec5, dimHavingSpec6); + assertEquals(dimHavingSpec7, dimHavingSpec8); + assertNotEquals(dimHavingSpec9, dimHavingSpec10); + assertNotEquals(dimHavingSpec11, dimHavingSpec12); + + } + + @Test + public void testToString() + { + String expected = "DimensionSelectorHavingSpec{dimension='dimension', value='v'}"; + + Assert.assertEquals(new DimensionSelectorHavingSpec("dimension", "v").toString(), expected); + } + + @Test(expected = NullPointerException.class) + public void testNullDimension() + { + new DimensionSelectorHavingSpec(null, "value"); + } + + @Test + public void testDimensionFilterSpec() + { + DimensionSelectorHavingSpec spec = new DimensionSelectorHavingSpec("dimension", "v"); + assertTrue(spec.eval(getTestRow("v"))); + assertTrue(spec.eval(getTestRow(ImmutableList.of("v", "v1")))); + assertFalse(spec.eval(getTestRow(ImmutableList.of()))); + assertFalse(spec.eval(getTestRow("v1"))); + + spec = new DimensionSelectorHavingSpec("dimension", null); + assertTrue(spec.eval(getTestRow(ImmutableList.of()))); + assertTrue(spec.eval(getTestRow(ImmutableList.of("")))); + assertFalse(spec.eval(getTestRow(ImmutableList.of("v")))); + assertFalse(spec.eval(getTestRow(ImmutableList.of("v", "v1")))); + + spec = new DimensionSelectorHavingSpec("dimension", ""); + assertTrue(spec.eval(getTestRow(ImmutableList.of()))); + assertTrue(spec.eval(getTestRow(ImmutableList.of("")))); + assertTrue(spec.eval(getTestRow(ImmutableList.of("v", "v1", "")))); + assertFalse(spec.eval(getTestRow(ImmutableList.of("v")))); + assertFalse(spec.eval(getTestRow(ImmutableList.of("v", "v1")))); + } + + @Test + public void testGetCacheKey() + { + byte[] dimBytes = "dimension".getBytes(Charsets.UTF_8); + byte[] valBytes = "v".getBytes(Charsets.UTF_8); + + byte[] expected = ByteBuffer.allocate(12) + .put(CACHE_KEY) + .put(dimBytes) + .put(STRING_SEPARATOR) + .put(valBytes) + .array(); + + DimensionSelectorHavingSpec dfhs = new DimensionSelectorHavingSpec("dimension", "v"); + DimensionSelectorHavingSpec dfhs1 = new DimensionSelectorHavingSpec("dimension", "v"); + DimensionSelectorHavingSpec dfhs2 = new DimensionSelectorHavingSpec("dimensi", "onv"); + + byte[] actual = dfhs.getCacheKey(); + + Assert.assertArrayEquals(expected, actual); + Assert.assertTrue(Arrays.equals(dfhs.getCacheKey(), dfhs1.getCacheKey())); + Assert.assertFalse(Arrays.equals(dfhs.getCacheKey(), dfhs2.getCacheKey())); + + + } +}