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