From 5fda8613ad6298b3e186fd22a4849a4bdd5838e9 Mon Sep 17 00:00:00 2001
From: sb89594 <147619318+sb89594@users.noreply.github.com>
Date: Fri, 8 Dec 2023 07:09:06 +0000
Subject: [PATCH] Feature: Add IPv6 Match Function (#15212)
---
docs/querying/math-expr.md | 7 +-
docs/querying/sql-functions.md | 10 +-
docs/querying/sql-scalar.md | 3 +
.../apache/druid/guice/ExpressionModule.java | 2 +
.../expression/IPv6AddressExprUtils.java | 81 +++++++++++
.../expression/IPv6AddressMatchExprMacro.java | 137 ++++++++++++++++++
.../expression/IPv6AddressExprUtilsTest.java | 136 +++++++++++++++++
.../IPv6AddressMatchExprMacroTest.java | 106 ++++++++++++++
.../IPv6AddressMatchOperatorConversion.java | 64 ++++++++
.../calcite/planner/DruidOperatorTable.java | 7 +
website/.spelling | 12 +-
11 files changed, 561 insertions(+), 4 deletions(-)
create mode 100644 processing/src/main/java/org/apache/druid/query/expression/IPv6AddressExprUtils.java
create mode 100644 processing/src/main/java/org/apache/druid/query/expression/IPv6AddressMatchExprMacro.java
create mode 100644 processing/src/test/java/org/apache/druid/query/expression/IPv6AddressExprUtilsTest.java
create mode 100644 processing/src/test/java/org/apache/druid/query/expression/IPv6AddressMatchExprMacroTest.java
create mode 100644 sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/IPv6AddressMatchOperatorConversion.java
diff --git a/docs/querying/math-expr.md b/docs/querying/math-expr.md
index 2de004618d2c..2550e30da96f 100644
--- a/docs/querying/math-expr.md
+++ b/docs/querying/math-expr.md
@@ -275,13 +275,16 @@ type of the result:
## IP address functions
-For the IPv4 address functions, the `address` argument accepts either an IPv4 dotted-decimal string (e.g., "192.168.0.1") or an IP address represented as a long (e.g., 3232235521). Format the `subnet` argument as an IPv4 address subnet in CIDR notation (e.g., "192.168.0.0/16").
+For the IPv4 address functions, the `address` argument accepts either an IPv4 dotted-decimal string (e.g. "192.168.0.1") or an IP address represented as a long (e.g. 3232235521). Format the `subnet` argument as an IPv4 address subnet in CIDR notation (e.g. "192.168.0.0/16").
+
+For the IPv6 address function, the `address` argument accepts a semicolon separated string (e.g. "75e9:efa4:29c6:85f6::232c"). The format of the `subnet` argument should be an IPv6 address subnet in CIDR notation (e.g. "75e9:efa4:29c6:85f6::/64").
| function | description |
| --- | --- |
-| ipv4_match(address, subnet) | Returns 1 if the `address` belongs to the `subnet` literal, else 0. If `address` is not a valid IPv4 address, then 0 is returned. This function is more efficient if `address` is a long instead of a string.|
+| ipv4_match(address, subnet) | Returns 1 if the IPv4 `address` belongs to the `subnet` literal, else 0. If `address` is not a valid IPv4 address, then 0 is returned. This function is more efficient if `address` is a long instead of a string.|
| ipv4_parse(address) | Parses `address` into an IPv4 address stored as a long. Returns `address` if it is already a valid IPv4 integer address. Returns null if `address` cannot be represented as an IPv4 address. |
| ipv4_stringify(address) | Converts `address` into an IPv4 address dotted-decimal string. Returns `address` if it is already a valid IPv4 dotted-decimal string. Returns null if `address` cannot be represented as an IPv4 address.|
+| ipv6_match(address, subnet) | Returns 1 if the IPv6 `address` belongs to the `subnet` literal, else 0. If `address` is not a valid IPv6 address, then 0 is returned.|
## Other functions
diff --git a/docs/querying/sql-functions.md b/docs/querying/sql-functions.md
index 47b8ca904342..40fabb8a6ed2 100644
--- a/docs/querying/sql-functions.md
+++ b/docs/querying/sql-functions.md
@@ -769,7 +769,7 @@ Finds whether a string is in a given expression, case-insensitive.
**Function type:** [Scalar, IP address](sql-scalar.md#ip-address-functions)
-Returns true if the `address` belongs to the `subnet` literal, else false.
+Returns true if the IPv4 `address` belongs to the `subnet` literal, else false.
## IPV4_PARSE
@@ -787,6 +787,14 @@ Parses `address` into an IPv4 address stored as an integer.
Converts `address` into an IPv4 address in dot-decimal notation.
+## IPV6_MATCH
+
+`IPV6_MATCH(address, subnet)`
+
+**Function type:** [Scalar, IP address](sql-scalar.md#ip-address-functions)
+
+Returns true if the IPv6 `address` belongs to the `subnet` literal, else false.
+
## JSON_KEYS
**Function type:** [JSON](sql-json-functions.md)
diff --git a/docs/querying/sql-scalar.md b/docs/querying/sql-scalar.md
index c9409dd07bfd..b6be95c02691 100644
--- a/docs/querying/sql-scalar.md
+++ b/docs/querying/sql-scalar.md
@@ -200,11 +200,14 @@ For the IPv4 address functions, the `address` argument can either be an IPv4 dot
(e.g., "192.168.0.1") or an IP address represented as an integer (e.g., 3232235521). The `subnet`
argument should be a string formatted as an IPv4 address subnet in CIDR notation (e.g., "192.168.0.0/16").
+For the IPv6 address function, the `address` argument accepts a semicolon separated string (e.g. "75e9:efa4:29c6:85f6::232c"). The format of the `subnet` argument should be an IPv6 address subnet in CIDR notation (e.g. "75e9:efa4:29c6:85f6::/64").
+
|Function|Notes|
|---|---|
|`IPV4_MATCH(address, subnet)`|Returns true if the `address` belongs to the `subnet` literal, else false. If `address` is not a valid IPv4 address, then false is returned. This function is more efficient if `address` is an integer instead of a string.|
|`IPV4_PARSE(address)`|Parses `address` into an IPv4 address stored as an integer . If `address` is an integer that is a valid IPv4 address, then it is passed through. Returns null if `address` cannot be represented as an IPv4 address.|
|`IPV4_STRINGIFY(address)`|Converts `address` into an IPv4 address dotted-decimal string. If `address` is a string that is a valid IPv4 address, then it is passed through. Returns null if `address` cannot be represented as an IPv4 address.|
+| IPV6_MATCH(address, subnet) | Returns 1 if the IPv6 `address` belongs to the `subnet` literal, else 0. If `address` is not a valid IPv6 address, then 0 is returned.|
## Sketch functions
diff --git a/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java b/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java
index db5aeaf17c21..7d740721670e 100644
--- a/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java
+++ b/processing/src/main/java/org/apache/druid/guice/ExpressionModule.java
@@ -32,6 +32,7 @@
import org.apache.druid.query.expression.IPv4AddressMatchExprMacro;
import org.apache.druid.query.expression.IPv4AddressParseExprMacro;
import org.apache.druid.query.expression.IPv4AddressStringifyExprMacro;
+import org.apache.druid.query.expression.IPv6AddressMatchExprMacro;
import org.apache.druid.query.expression.LikeExprMacro;
import org.apache.druid.query.expression.NestedDataExpressions;
import org.apache.druid.query.expression.RegexpExtractExprMacro;
@@ -59,6 +60,7 @@ public class ExpressionModule implements Module
.add(IPv4AddressMatchExprMacro.class)
.add(IPv4AddressParseExprMacro.class)
.add(IPv4AddressStringifyExprMacro.class)
+ .add(IPv6AddressMatchExprMacro.class)
.add(LikeExprMacro.class)
.add(RegexpExtractExprMacro.class)
.add(RegexpLikeExprMacro.class)
diff --git a/processing/src/main/java/org/apache/druid/query/expression/IPv6AddressExprUtils.java b/processing/src/main/java/org/apache/druid/query/expression/IPv6AddressExprUtils.java
new file mode 100644
index 000000000000..17ef4131da32
--- /dev/null
+++ b/processing/src/main/java/org/apache/druid/query/expression/IPv6AddressExprUtils.java
@@ -0,0 +1,81 @@
+/*
+ * 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.expression;
+
+import inet.ipaddr.IPAddressString;
+import inet.ipaddr.IPAddressStringParameters;
+import inet.ipaddr.ipv6.IPv6Address;
+
+import javax.annotation.Nullable;
+
+public class IPv6AddressExprUtils
+{
+
+ private static final IPAddressStringParameters IPV6_ADDRESS_PARAMS = new IPAddressStringParameters.Builder().allowSingleSegment(false).allow_inet_aton(false).allowIPv4(false).allowPrefix(false).allowEmpty(false).toParams();
+ private static final IPAddressStringParameters IPV6_SUBNET_PARAMS = new IPAddressStringParameters.Builder().allowSingleSegment(false).allow_inet_aton(false).allowEmpty(false).allowIPv4(false).toParams();
+
+ /**
+ * @return True if argument is a valid IPv6 address semicolon separated string. Single segments, Inet addresses and subnets are not allowed.
+ */
+ static boolean isValidIPv6Address(@Nullable String addressString)
+ {
+ return addressString != null && new IPAddressString(addressString, IPV6_ADDRESS_PARAMS).isIPv6();
+ }
+
+ /**
+ * @return True if argument is a valid IPv6 subnet address.
+ */
+ static boolean isValidIPv6Subnet(@Nullable String subnetString)
+ {
+ return subnetString != null && new IPAddressString(subnetString, IPV6_SUBNET_PARAMS).isPrefixed();
+ }
+
+ /**
+ * @return IPv6 address if the supplied string is a valid semicolon separated IPv6 Address string.
+ */
+ @Nullable
+ public static IPv6Address parse(@Nullable String string)
+ {
+ IPAddressString ipAddressString = new IPAddressString(string, IPV6_ADDRESS_PARAMS);
+ if (ipAddressString.isIPv6()) {
+ return ipAddressString.getAddress().toIPv6();
+ }
+ return null;
+ }
+
+ @Nullable
+ public static IPAddressString parseString(@Nullable String string)
+ {
+ IPAddressString ipAddressString = new IPAddressString(string, IPV6_ADDRESS_PARAMS);
+ if (ipAddressString.isIPv6()) {
+ return ipAddressString;
+ }
+ return null;
+ }
+
+ /**
+ * @return IPv6 address from supplied array of bytes
+ */
+ @Nullable
+ public static IPv6Address parse(@Nullable byte[] bytes)
+ {
+ return bytes == null ? null : new IPv6Address(bytes);
+ }
+}
diff --git a/processing/src/main/java/org/apache/druid/query/expression/IPv6AddressMatchExprMacro.java b/processing/src/main/java/org/apache/druid/query/expression/IPv6AddressMatchExprMacro.java
new file mode 100644
index 000000000000..8f1f3b82c129
--- /dev/null
+++ b/processing/src/main/java/org/apache/druid/query/expression/IPv6AddressMatchExprMacro.java
@@ -0,0 +1,137 @@
+/*
+ * 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.expression;
+
+import inet.ipaddr.IPAddressString;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.math.expr.Expr;
+import org.apache.druid.math.expr.ExprEval;
+import org.apache.druid.math.expr.ExprMacroTable;
+import org.apache.druid.math.expr.ExpressionType;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ *
+ * Implements an expression that checks if an IPv6 address belongs to a subnet.
+ *
+ * Expression signatures:
+ * - long ipv6_match(string address, string subnet)
+ *
+ * Valid "address" argument formats are:
+ * - IPv6 address string (e.g., "2001:4860:4860::8888")
+ *
+ * The argument format for the "subnet" argument should be a literal in CIDR notation
+ * (e.g., "2001:db8::/64 ").
+ *
+ * If the "address" argument does not represent an IPv6 address then false is returned.
+ *
+ *
+*/
+public class IPv6AddressMatchExprMacro implements ExprMacroTable.ExprMacro
+{
+ public static final String FN_NAME = "ipv6_match";
+ private static final int ARG_SUBNET = 1;
+
+ @Override
+ public String name()
+ {
+ return FN_NAME;
+ }
+
+ @Override
+ public Expr apply(final List args)
+ {
+ validationHelperCheckArgumentCount(args, 2);
+
+ try {
+ final Expr arg = args.get(0);
+ final IPAddressString blockString = getSubnetInfo(args);
+
+ class IPv6AddressMatchExpr extends ExprMacroTable.BaseScalarUnivariateMacroFunctionExpr
+ {
+ private IPv6AddressMatchExpr(Expr arg)
+ {
+ super(FN_NAME, arg);
+ }
+
+ @Nonnull
+ @Override
+ public ExprEval eval(final ObjectBinding bindings)
+ {
+ ExprEval eval = arg.eval(bindings);
+ boolean match;
+ switch (eval.type().getType()) {
+ case STRING:
+ match = isStringMatch(eval.asString());
+ break;
+ default:
+ match = false;
+ }
+ return ExprEval.ofLongBoolean(match);
+ }
+
+ private boolean isStringMatch(String stringValue)
+ {
+ IPAddressString addressString = IPv6AddressExprUtils.parseString(stringValue);
+ return addressString != null && blockString.prefixContains(addressString);
+ }
+
+ @Override
+ public Expr visit(Shuttle shuttle)
+ {
+ return shuttle.visit(apply(shuttle.visitAll(args)));
+ }
+
+ @Override
+ public String stringify()
+ {
+ return StringUtils.format("%s(%s, %s)", FN_NAME, arg.stringify(), args.get(ARG_SUBNET).stringify());
+ }
+
+ @Nullable
+ @Override
+ public ExpressionType getOutputType(InputBindingInspector inspector)
+ {
+ return ExpressionType.LONG;
+ }
+ }
+
+ return new IPv6AddressMatchExpr(arg);
+ }
+ catch (Exception e) {
+ throw processingFailed(e, "failed to parse address");
+ }
+ }
+
+ private IPAddressString getSubnetInfo(List args)
+ {
+ String subnetArgName = "subnet";
+ Expr arg = args.get(ARG_SUBNET);
+ validationHelperCheckArgIsLiteral(arg, subnetArgName);
+ String subnet = (String) arg.getLiteralValue();
+ if (!IPv6AddressExprUtils.isValidIPv6Subnet(subnet)) {
+ throw validationFailed(subnetArgName + " arg has an invalid format: " + subnet);
+ }
+ return new IPAddressString(subnet);
+ }
+}
diff --git a/processing/src/test/java/org/apache/druid/query/expression/IPv6AddressExprUtilsTest.java b/processing/src/test/java/org/apache/druid/query/expression/IPv6AddressExprUtilsTest.java
new file mode 100644
index 000000000000..f33c8aed3672
--- /dev/null
+++ b/processing/src/test/java/org/apache/druid/query/expression/IPv6AddressExprUtilsTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.expression;
+
+import inet.ipaddr.ipv6.IPv6Address;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class IPv6AddressExprUtilsTest
+{
+ private static final List VALID_IPV6_ADDRESSES = Arrays.asList(
+ "2001:db8:85a3:8d3:1319:8a2e:370:7348",
+ "::",
+ "2001::8a2e:370:7348",
+ "::8a2e:370:7348"
+ );
+
+ private static final List INVALID_IPV6_ADDRESSES = Arrays.asList(
+ "druid.apache.org", // URL
+ "190.0.0.0", // IPv4 address
+ "gff2::8a2e:370:7348", // first octet exceeds max size
+ "023a:8a2e:7348", // invalid address length
+ "2001:0db8:/32" // CIDR
+ );
+
+ private static final List VALID_IPV6_SUBNETS = Arrays.asList(
+ "2001:db8:85a3:8d3::/64",
+ "2001:db8::/8"
+ );
+
+ private static final List INVALID_IPV6_SUBNETS = Arrays.asList(
+ "2001:db8:85a3::/129", // subnet mask too large
+ "f3ed::" // no subnet mask
+ );
+
+ @Test
+ public void testIsValidIPv6AddressNull()
+ {
+ Assert.assertFalse(IPv6AddressExprUtils.isValidIPv6Address(null));
+ }
+
+ @Test
+ public void testIsValidIPv6Address()
+ {
+ for (String address : VALID_IPV6_ADDRESSES) {
+ Assert.assertTrue(getErrMsg(address), IPv6AddressExprUtils.isValidIPv6Address(address));
+ }
+ }
+
+ @Test
+ public void testIsValidIPv6AddressNotIpAddress()
+ {
+ for (String address : INVALID_IPV6_ADDRESSES) {
+ Assert.assertFalse(getErrMsg(address), IPv6AddressExprUtils.isValidIPv6Address(address));
+ }
+ }
+
+ @Test
+ public void testIsValidSubnetNull()
+ {
+ Assert.assertFalse(IPv6AddressExprUtils.isValidIPv6Subnet(null));
+ }
+
+ @Test
+ public void testIsValidIPv6SubnetValid()
+ {
+ for (String address : VALID_IPV6_SUBNETS) {
+ Assert.assertTrue(getErrMsg(address), IPv6AddressExprUtils.isValidIPv6Subnet(address));
+ }
+ }
+
+ @Test
+ public void testIsValidIPv6SubnetInvalid()
+ {
+ for (String address : INVALID_IPV6_SUBNETS) {
+ Assert.assertFalse(getErrMsg(address), IPv6AddressExprUtils.isValidIPv6Subnet(address));
+ }
+ }
+
+ @Test
+ public void testParseNullString()
+ {
+ Assert.assertNull(IPv6AddressExprUtils.parse((String) null));
+ }
+
+ @Test
+ public void testParseNullBytes()
+ {
+ Assert.assertNull(IPv6AddressExprUtils.parse((byte[]) null));
+ }
+
+ @Test
+ public void testParseIPv6()
+ {
+ for (String string : VALID_IPV6_ADDRESSES) {
+ String errMsg = getErrMsg(string);
+ IPv6Address address = IPv6AddressExprUtils.parse(string);
+ Assert.assertNotNull(errMsg, address);
+ Assert.assertEquals(errMsg, string, address.toString());
+ }
+ }
+
+ @Test
+ public void testParseNotIpV6Addresses()
+ {
+ for (String address : INVALID_IPV6_ADDRESSES) {
+ Assert.assertNull(getErrMsg(address), IPv6AddressExprUtils.parse(address));
+ }
+ }
+
+ private String getErrMsg(String msg)
+ {
+ String prefix = "Failed: ";
+ return prefix + msg;
+ }
+}
diff --git a/processing/src/test/java/org/apache/druid/query/expression/IPv6AddressMatchExprMacroTest.java b/processing/src/test/java/org/apache/druid/query/expression/IPv6AddressMatchExprMacroTest.java
new file mode 100644
index 000000000000..d8371298dcfa
--- /dev/null
+++ b/processing/src/test/java/org/apache/druid/query/expression/IPv6AddressMatchExprMacroTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.expression;
+
+import org.apache.druid.math.expr.Expr;
+import org.apache.druid.math.expr.ExprEval;
+import org.apache.druid.math.expr.ExpressionProcessingException;
+import org.apache.druid.math.expr.ExpressionValidationException;
+import org.apache.druid.math.expr.InputBindings;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+public class IPv6AddressMatchExprMacroTest extends MacroTestBase
+{
+ private static final Expr IPV6 = ExprEval.of("201:ef:168::").toExpr();
+ private static final Expr IPV6_CIDR = ExprEval.of("201:ef:168::/32").toExpr();
+
+ public IPv6AddressMatchExprMacroTest()
+ {
+ super(new IPv6AddressMatchExprMacro());
+ }
+
+ @Test
+ public void testTooFewArgs()
+ {
+ expectException(ExpressionValidationException.class, "requires 2 arguments");
+ apply(Collections.emptyList());
+ }
+
+ @Test
+ public void testTooManyArgs()
+ {
+ Expr extraArgument = ExprEval.of("An extra argument").toExpr();
+ expectException(ExpressionValidationException.class, "requires 2 arguments");
+ apply(Arrays.asList(IPV6, IPV6_CIDR, extraArgument));
+ }
+
+ @Test
+ public void testSubnetArgInvalid()
+ {
+ expectException(ExpressionProcessingException.class, "Function[ipv6_match] failed to parse address");
+ Expr invalidSubnet = ExprEval.of("201:ef:168::/invalid").toExpr();
+ apply(Arrays.asList(IPV6, invalidSubnet));
+ }
+
+ @Test
+ public void testNullStringArg()
+ {
+ Expr nullString = ExprEval.of(null).toExpr();
+ Assert.assertFalse(eval(nullString, IPV6_CIDR));
+ }
+
+ @Test
+ public void testMatchingStringArgIPv6()
+ {
+ Assert.assertTrue(eval(IPV6, IPV6_CIDR));
+ }
+
+ @Test
+ public void testNotMatchingStringArgIPv6()
+ {
+ Expr nonMatchingIpv6 = ExprEval.of("2002:ef:168::").toExpr();
+ Assert.assertFalse(eval(nonMatchingIpv6, IPV6_CIDR));
+ }
+
+ @Test
+ public void testNotIpAddress()
+ {
+ Expr notIpAddress = ExprEval.of("druid.apache.org").toExpr();
+ Assert.assertFalse(eval(notIpAddress, IPV6_CIDR));
+ }
+
+ @Test
+ public void testInclusive()
+ {
+ Expr subnet = IPV6_CIDR;
+ Assert.assertTrue(eval(IPV6, subnet));
+ }
+
+ private boolean eval(Expr... args)
+ {
+ Expr expr = apply(Arrays.asList(args));
+ ExprEval eval = expr.eval(InputBindings.nilBindings());
+ return eval.asBoolean();
+ }
+}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/IPv6AddressMatchOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/IPv6AddressMatchOperatorConversion.java
new file mode 100644
index 000000000000..1737df5b5f5d
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/IPv6AddressMatchOperatorConversion.java
@@ -0,0 +1,64 @@
+/*
+ * 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.sql.calcite.expression.builtin;
+
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlSingleOperandTypeChecker;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.query.expression.IPv6AddressMatchExprMacro;
+import org.apache.druid.sql.calcite.expression.DirectOperatorConversion;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
+
+public class IPv6AddressMatchOperatorConversion extends DirectOperatorConversion
+{
+ private static final SqlSingleOperandTypeChecker ADDRESS_OPERAND = OperandTypes.or(
+ OperandTypes.family(SqlTypeFamily.STRING)
+ );
+
+ private static final SqlSingleOperandTypeChecker SUBNET_OPERAND = OperandTypes.family(SqlTypeFamily.STRING);
+
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder(StringUtils.toUpperCase(IPv6AddressMatchExprMacro.FN_NAME))
+ .operandTypeChecker(
+ OperandTypes.sequence(
+ "'" + StringUtils.toUpperCase(IPv6AddressMatchExprMacro.FN_NAME) + "(expr, string)'",
+ ADDRESS_OPERAND,
+ SUBNET_OPERAND
+ ))
+ .returnTypeInference(ReturnTypes.BOOLEAN_NULLABLE)
+ .functionCategory(SqlFunctionCategory.USER_DEFINED_FUNCTION)
+ .build();
+
+ public IPv6AddressMatchOperatorConversion()
+ {
+ super(SQL_FUNCTION, IPv6AddressMatchExprMacro.FN_NAME);
+ }
+
+ @Override
+ public SqlOperator calciteOperator()
+ {
+ return SQL_FUNCTION;
+ }
+}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
index 6c432c800d89..ef866aa23cb6 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
@@ -87,6 +87,7 @@
import org.apache.druid.sql.calcite.expression.builtin.IPv4AddressMatchOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.IPv4AddressParseOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.IPv4AddressStringifyOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.IPv6AddressMatchOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.LPadOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.LTrimOperatorConversion;
import org.apache.druid.sql.calcite.expression.builtin.LeastOperatorConversion;
@@ -294,6 +295,11 @@ public class DruidOperatorTable implements SqlOperatorTable
.add(new IPv4AddressStringifyOperatorConversion())
.build();
+ private static final List IPV6ADDRESS_OPERATOR_CONVERSIONS =
+ ImmutableList.builder()
+ .add(new IPv6AddressMatchOperatorConversion())
+ .build();
+
private static final List FORMAT_OPERATOR_CONVERSIONS =
ImmutableList.builder()
.add(HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT)
@@ -407,6 +413,7 @@ public class DruidOperatorTable implements SqlOperatorTable
.addAll(MULTIVALUE_STRING_OPERATOR_CONVERSIONS)
.addAll(REDUCTION_OPERATOR_CONVERSIONS)
.addAll(IPV4ADDRESS_OPERATOR_CONVERSIONS)
+ .addAll(IPV6ADDRESS_OPERATOR_CONVERSIONS)
.addAll(FORMAT_OPERATOR_CONVERSIONS)
.addAll(BITWISE_OPERATOR_CONVERSIONS)
.addAll(CUSTOM_MATH_OPERATOR_CONVERSIONS)
diff --git a/website/.spelling b/website/.spelling
index b7e7bc74e501..e5b8f3cb494e 100644
--- a/website/.spelling
+++ b/website/.spelling
@@ -117,6 +117,7 @@ IcebergInputSource
IETF
IP
IPv4
+IPv6
IS_AGGREGATOR
IS_BROADCAST
IS_JOINABLE
@@ -1490,8 +1491,17 @@ getExponent
hypot
ipv4_match
ipv4_parse
-isnull
ipv4_stringify
+ipv6_match
+
+# IPv6 Address Example Sections
+75e9
+efa4
+29c6
+85f6
+232c
+
+isnull
java.lang.Math
java.lang.String
JNA