This is an automated email from the ASF dual-hosted git repository.
junhao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-trino.git
The following commit(s) were added to refs/heads/main by this push:
new 652d2a7 Support trino query paimon file index for map type with
map-keys (#67)
652d2a7 is described below
commit 652d2a797141ae6b9ff0aa30eff3ed131fae808a
Author: Jason Zhang <[email protected]>
AuthorDate: Mon May 6 14:49:19 2024 +0800
Support trino query paimon file index for map type with map-keys (#67)
---------
Co-authored-by: yilong.zyl <[email protected]>
Co-authored-by: yejunhao <[email protected]>
---
.../apache/paimon/trino/TrinoFilterConverter.java | 45 +++-
.../apache/paimon/trino/TrinoFilterExtractor.java | 242 +++++++++++++++++++++
.../org/apache/paimon/trino/TrinoMetadata.java | 41 +---
.../apache/paimon/trino/catalog/TrinoCatalog.java | 25 +++
.../paimon/trino/TestTrinoFilterExtractor.java | 96 ++++++++
pom.xml | 16 +-
6 files changed, 428 insertions(+), 37 deletions(-)
diff --git
a/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoFilterConverter.java
b/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoFilterConverter.java
index a690fca..50060b7 100644
---
a/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoFilterConverter.java
+++
b/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoFilterConverter.java
@@ -21,6 +21,9 @@ package org.apache.paimon.trino;
import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.Decimal;
import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.fileindex.FileIndexOptions;
+import org.apache.paimon.predicate.In;
+import org.apache.paimon.predicate.LeafPredicate;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.predicate.PredicateBuilder;
import org.apache.paimon.types.RowType;
@@ -103,10 +106,20 @@ public class TrinoFilterConverter {
TrinoColumnHandle columnHandle = entry.getKey();
Domain domain = entry.getValue();
String field = columnHandle.getColumnName();
+ Optional<Integer> nestedColumn =
FileIndexOptions.topLevelIndexOfNested(field);
+ if (nestedColumn.isPresent()) {
+ int position = nestedColumn.get();
+ field = field.substring(0, position);
+ }
int index = fieldNames.indexOf(field);
if (index != -1) {
try {
- conjuncts.add(toPredicate(index,
columnHandle.getTrinoType(), domain));
+ conjuncts.add(
+ toPredicate(
+ index,
+ columnHandle.getColumnName(),
+ columnHandle.getTrinoType(),
+ domain));
acceptedDomains.put(columnHandle, domain);
continue;
} catch (UnsupportedOperationException exception) {
@@ -124,7 +137,7 @@ public class TrinoFilterConverter {
return Optional.of(and(conjuncts));
}
- private Predicate toPredicate(int columnIndex, Type type, Domain domain) {
+ private Predicate toPredicate(int columnIndex, String field, Type type,
Domain domain) {
if (domain.isAll()) {
// TODO alwaysTrue
throw new UnsupportedOperationException();
@@ -146,13 +159,35 @@ public class TrinoFilterConverter {
}
// TODO support structural types
- if (type instanceof ArrayType
- || type instanceof MapType
- || type instanceof io.trino.spi.type.RowType) {
+ if (type instanceof ArrayType || type instanceof
io.trino.spi.type.RowType) {
// Fail fast. Ignoring expression could lead to data loss in case
of deletions.
throw new UnsupportedOperationException();
}
+ if (type instanceof MapType) {
+ List<Range> orderedRanges =
domain.getValues().getRanges().getOrderedRanges();
+ List<Object> values = new ArrayList<>();
+ List<Predicate> predicates = new ArrayList<>();
+ for (Range range : orderedRanges) {
+ if (range.isSingleValue()) {
+ values.add(
+ getLiteralValue(
+ ((MapType) type).getValueType(),
range.getLowBoundedValue()));
+ }
+ }
+ if (!values.isEmpty()) {
+ Predicate predicate =
+ new LeafPredicate(
+ In.INSTANCE,
+ TrinoTypeUtils.toPaimonType(type),
+ columnIndex,
+ field,
+ values);
+ predicates.add(predicate);
+ }
+ return or(predicates);
+ }
+
if (type.isOrderable()) {
List<Range> orderedRanges =
domain.getValues().getRanges().getOrderedRanges();
List<Object> values = new ArrayList<>();
diff --git
a/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoFilterExtractor.java
b/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoFilterExtractor.java
new file mode 100644
index 0000000..95e5957
--- /dev/null
+++
b/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoFilterExtractor.java
@@ -0,0 +1,242 @@
+/*
+ * 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.paimon.trino;
+
+import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList;
+import org.apache.paimon.shade.guava30.com.google.common.collect.Maps;
+import org.apache.paimon.trino.catalog.TrinoCatalog;
+
+import io.airlift.slice.Slice;
+import io.trino.spi.connector.ColumnHandle;
+import io.trino.spi.connector.Constraint;
+import io.trino.spi.expression.Call;
+import io.trino.spi.expression.Constant;
+import io.trino.spi.expression.Variable;
+import io.trino.spi.predicate.Domain;
+import io.trino.spi.predicate.Range;
+import io.trino.spi.predicate.SortedRangeSet;
+import io.trino.spi.predicate.TupleDomain;
+import io.trino.spi.type.ArrayType;
+import io.trino.spi.type.MapType;
+import io.trino.spi.type.Type;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static io.trino.spi.expression.StandardFunctions.AND_FUNCTION_NAME;
+import static
io.trino.spi.expression.StandardFunctions.EQUAL_OPERATOR_FUNCTION_NAME;
+import static
io.trino.spi.expression.StandardFunctions.IN_PREDICATE_FUNCTION_NAME;
+import static org.apache.paimon.fileindex.FileIndexCommon.toMapKey;
+
+/** Extract filter from trino. */
+public class TrinoFilterExtractor {
+
+ public static final String TRINO_MAP_ELEMENT_AT_FUNCTION_NAME =
"element_at";
+
+ /** Extract filter from trino , include ExpressionFilter. */
+ public static Optional<TrinoFilter> extract(
+ TrinoCatalog catalog, TrinoTableHandle trinoTableHandle,
Constraint constraint) {
+
+ TupleDomain<TrinoColumnHandle> oldFilter =
trinoTableHandle.getFilter();
+ TupleDomain<TrinoColumnHandle> newFilter =
+ constraint
+ .getSummary()
+ .transformKeys(TrinoColumnHandle.class::cast)
+ .intersect(oldFilter);
+
+ if (oldFilter.equals(newFilter)) {
+ return Optional.empty();
+ }
+
+ Map<TrinoColumnHandle, Domain> trinoColumnHandleForExpressionFilter =
+ extractTrinoColumnHandleForExpressionFilter(constraint);
+
+ LinkedHashMap<TrinoColumnHandle, Domain> acceptedDomains = new
LinkedHashMap<>();
+ LinkedHashMap<TrinoColumnHandle, Domain> unsupportedDomains = new
LinkedHashMap<>();
+ new TrinoFilterConverter(trinoTableHandle.table(catalog).rowType())
+ .convert(newFilter, acceptedDomains, unsupportedDomains);
+
+ List<String> partitionKeys =
trinoTableHandle.table(catalog).partitionKeys();
+ LinkedHashMap<TrinoColumnHandle, Domain> unenforcedDomains = new
LinkedHashMap<>();
+ acceptedDomains.forEach(
+ (columnHandle, domain) -> {
+ if (!partitionKeys.contains(columnHandle.getColumnName()))
{
+ unenforcedDomains.put(columnHandle, domain);
+ }
+ });
+
+ acceptedDomains.putAll(trinoColumnHandleForExpressionFilter);
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ TupleDomain<ColumnHandle> remain =
+ (TupleDomain)
+ TupleDomain.withColumnDomains(unsupportedDomains)
+
.intersect(TupleDomain.withColumnDomains(unenforcedDomains));
+
+ return Optional.of(new
TrinoFilter(TupleDomain.withColumnDomains(acceptedDomains), remain));
+ }
+
+ /**
+ * Extract Expression filter from trino Constraint. Extract Trino
Expression filter ( e.g.
+ * element_at(jsonmap, 'a') = '1' ) to TrinoColumnHandle.
+ */
+ public static Map<TrinoColumnHandle, Domain>
extractTrinoColumnHandleForExpressionFilter(
+ Constraint constraint) {
+ Map<TrinoColumnHandle, Domain> expressionPredicates =
Collections.emptyMap();
+
+ if (constraint.getExpression() instanceof Call) {
+ Call expression = (Call) constraint.getExpression();
+ Map<String, ColumnHandle> assignments =
constraint.getAssignments();
+
+ if
(expression.getFunctionName().equals(EQUAL_OPERATOR_FUNCTION_NAME)) {
+ expressionPredicates = handleExpressionEqualOrIn(assignments,
expression, false);
+ } else if
(expression.getFunctionName().equals(IN_PREDICATE_FUNCTION_NAME)) {
+ expressionPredicates = handleExpressionEqualOrIn(assignments,
expression, true);
+ } else if (expression.getFunctionName().equals(AND_FUNCTION_NAME))
{
+ expressionPredicates = handleAndArguments(assignments,
expression);
+ }
+ // TODO: Support "or" clause
+ }
+ return expressionPredicates;
+ }
+
+ /** Expression filter support the case of "AND" and "IN". */
+ private static Map<TrinoColumnHandle, Domain> handleAndArguments(
+ Map<String, ColumnHandle> assignments, Call expression) {
+ Map<TrinoColumnHandle, Domain> expressionPredicates = new HashMap<>();
+
+ expression.getArguments().stream()
+ .map(argument -> (Call) argument)
+ .forEach(
+ argument -> {
+ if
(argument.getFunctionName().equals(EQUAL_OPERATOR_FUNCTION_NAME)) {
+ expressionPredicates.putAll(
+ handleExpressionEqualOrIn(assignments,
argument, false));
+ } else if (argument.getFunctionName()
+ .equals(IN_PREDICATE_FUNCTION_NAME)) {
+ expressionPredicates.putAll(
+ handleExpressionEqualOrIn(assignments,
argument, true));
+ }
+ });
+
+ return expressionPredicates;
+ }
+
+ private static Map<TrinoColumnHandle, Domain> handleExpressionEqualOrIn(
+ Map<String, ColumnHandle> assignments, Call expression, boolean
inClause) {
+
+ Call elementAtExpression = (Call) expression.getArguments().get(0);
+
+ String functionName = elementAtExpression.getFunctionName().getName();
+
+ switch (functionName) {
+ case TRINO_MAP_ELEMENT_AT_FUNCTION_NAME:
+ {
+ Variable columnExpression =
+ (Variable)
elementAtExpression.getArguments().get(0);
+ Constant columnKey = (Constant)
elementAtExpression.getArguments().get(1);
+
+ Constant elementAtValue = (Constant)
expression.getArguments().get(1);
+ List<Range> values;
+ Type elementType;
+ if (inClause) {
+ elementType = ((ArrayType)
elementAtValue.getType()).getElementType();
+ values =
+ elementAtValue.getChildren().stream()
+ .filter(a -> ((Constant) a).getValue()
!= null)
+ .map(
+ arguemnt ->
+ Range.equal(
+
arguemnt.getType(),
+ ((Constant)
arguemnt).getValue()))
+ .collect(Collectors.toList());
+ } else {
+ elementType = elementAtValue.getType();
+ values =
+ elementAtValue.getValue() == null
+ ? Collections.emptyList()
+ : ImmutableList.of(
+ Range.equal(
+
elementAtValue.getType(),
+
elementAtValue.getValue()));
+ }
+ if (columnKey.getValue() == null) {
+ throw new RuntimeException("Expression pares failed: "
+ expression);
+ }
+
+ return handleElementAtArguments(
+ assignments,
+ columnExpression.getName(),
+ ((Slice) columnKey.getValue()).toStringUtf8(),
+ elementType,
+ values);
+ }
+ default:
+ {
+ return Collections.emptyMap();
+ }
+ }
+ }
+
+ /** Using paimon, trino only supports element_at function to extract
values from map type. */
+ private static Map<TrinoColumnHandle, Domain> handleElementAtArguments(
+ Map<String, ColumnHandle> assignments,
+ String columnName,
+ String nestedName,
+ Type elementType,
+ List<Range> ranges) {
+ Map<TrinoColumnHandle, Domain> expressionPredicates =
Maps.newHashMap();
+ TrinoColumnHandle trinoColumnHandle = (TrinoColumnHandle)
assignments.get(columnName);
+ Type trinoType = trinoColumnHandle.getTrinoType();
+ if (trinoType instanceof MapType) {
+ expressionPredicates.put(
+ TrinoColumnHandle.of(
+ toMapKey(columnName, nestedName),
+ TrinoTypeUtils.toPaimonType(trinoType)),
+ Domain.create(SortedRangeSet.copyOf(elementType, ranges),
false));
+ }
+ return expressionPredicates;
+ }
+
+ /** TrinoFilter for paimon trinoMetadata applyFilter. */
+ public static class TrinoFilter {
+
+ private final TupleDomain<TrinoColumnHandle> filter;
+ private final TupleDomain<ColumnHandle> remainFilter;
+
+ public TrinoFilter(
+ TupleDomain<TrinoColumnHandle> filter,
TupleDomain<ColumnHandle> remainFilter) {
+ this.filter = filter;
+ this.remainFilter = remainFilter;
+ }
+
+ public TupleDomain<TrinoColumnHandle> getFilter() {
+ return filter;
+ }
+
+ public TupleDomain<ColumnHandle> getRemainFilter() {
+ return remainFilter;
+ }
+ }
+}
diff --git
a/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoMetadata.java
b/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoMetadata.java
index 0d8510e..926ca64 100644
---
a/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoMetadata.java
+++
b/paimon-trino-common/src/main/java/org/apache/paimon/trino/TrinoMetadata.java
@@ -45,7 +45,6 @@ import io.trino.spi.connector.SchemaTableName;
import io.trino.spi.connector.SchemaTablePrefix;
import io.trino.spi.expression.ConnectorExpression;
import io.trino.spi.predicate.Domain;
-import io.trino.spi.predicate.TupleDomain;
import io.trino.spi.security.TrinoPrincipal;
import io.trino.spi.type.LongTimestampWithTimeZone;
import io.trino.spi.type.TimestampWithTimeZoneType;
@@ -423,38 +422,18 @@ public class TrinoMetadata implements ConnectorMetadata {
ConnectorSession session, ConnectorTableHandle handle, Constraint
constraint) {
catalog.initSession(session);
TrinoTableHandle trinoTableHandle = (TrinoTableHandle) handle;
- TupleDomain<TrinoColumnHandle> oldFilter =
trinoTableHandle.getFilter();
- TupleDomain<TrinoColumnHandle> newFilter =
- constraint
- .getSummary()
- .transformKeys(TrinoColumnHandle.class::cast)
- .intersect(oldFilter);
- if (oldFilter.equals(newFilter)) {
+ Optional<TrinoFilterExtractor.TrinoFilter> extract =
+ TrinoFilterExtractor.extract(catalog, trinoTableHandle,
constraint);
+ if (extract.isPresent()) {
+ TrinoFilterExtractor.TrinoFilter trinoFilter = extract.get();
+ return Optional.of(
+ new ConstraintApplicationResult<>(
+ trinoTableHandle.copy(trinoFilter.getFilter()),
+ trinoFilter.getRemainFilter(),
+ false));
+ } else {
return Optional.empty();
}
-
- LinkedHashMap<TrinoColumnHandle, Domain> acceptedDomains = new
LinkedHashMap<>();
- LinkedHashMap<TrinoColumnHandle, Domain> unsupportedDomains = new
LinkedHashMap<>();
- new TrinoFilterConverter(trinoTableHandle.table(catalog).rowType())
- .convert(newFilter, acceptedDomains, unsupportedDomains);
-
- List<String> partitionKeys =
trinoTableHandle.table(catalog).partitionKeys();
- LinkedHashMap<TrinoColumnHandle, Domain> unenforcedDomains = new
LinkedHashMap<>();
- acceptedDomains.forEach(
- (columnHandle, domain) -> {
- if (!partitionKeys.contains(columnHandle.getColumnName()))
{
- unenforcedDomains.put(columnHandle, domain);
- }
- });
-
- @SuppressWarnings({"unchecked", "rawtypes"})
- TupleDomain<ColumnHandle> remain =
- (TupleDomain)
- TupleDomain.withColumnDomains(unsupportedDomains)
-
.intersect(TupleDomain.withColumnDomains(unenforcedDomains));
-
- return Optional.of(
- new
ConstraintApplicationResult<>(trinoTableHandle.copy(newFilter), remain, false));
}
@Override
diff --git
a/paimon-trino-common/src/main/java/org/apache/paimon/trino/catalog/TrinoCatalog.java
b/paimon-trino-common/src/main/java/org/apache/paimon/trino/catalog/TrinoCatalog.java
index e02abb4..2caab17 100644
---
a/paimon-trino-common/src/main/java/org/apache/paimon/trino/catalog/TrinoCatalog.java
+++
b/paimon-trino-common/src/main/java/org/apache/paimon/trino/catalog/TrinoCatalog.java
@@ -26,6 +26,7 @@ import org.apache.paimon.catalog.CatalogFactory;
import org.apache.paimon.catalog.CatalogLockContext;
import org.apache.paimon.catalog.CatalogLockFactory;
import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.fs.FileIO;
import org.apache.paimon.metastore.MetastoreClient;
import org.apache.paimon.options.Options;
import org.apache.paimon.schema.Schema;
@@ -95,6 +96,30 @@ public class TrinoCatalog implements Catalog {
}
}
+ @Override
+ public String warehouse() {
+ if (!inited) {
+ throw new RuntimeException("Not inited yet.");
+ }
+ return current.warehouse();
+ }
+
+ @Override
+ public Map<String, String> options() {
+ if (!inited) {
+ throw new RuntimeException("Not inited yet.");
+ }
+ return current.options();
+ }
+
+ @Override
+ public FileIO fileIO() {
+ if (!inited) {
+ throw new RuntimeException("Not inited yet.");
+ }
+ return current.fileIO();
+ }
+
@Override
public Optional<CatalogLockFactory> lockFactory() {
return current.lockFactory();
diff --git
a/paimon-trino-common/src/test/java/org/apache/paimon/trino/TestTrinoFilterExtractor.java
b/paimon-trino-common/src/test/java/org/apache/paimon/trino/TestTrinoFilterExtractor.java
new file mode 100644
index 0000000..a54dbef
--- /dev/null
+++
b/paimon-trino-common/src/test/java/org/apache/paimon/trino/TestTrinoFilterExtractor.java
@@ -0,0 +1,96 @@
+/*
+ * 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.paimon.trino;
+
+import org.apache.paimon.shade.guava30.com.google.common.collect.Maps;
+import org.apache.paimon.types.DataTypes;
+
+import io.airlift.slice.Slice;
+import io.airlift.slice.Slices;
+import io.trino.spi.connector.ColumnHandle;
+import io.trino.spi.connector.Constraint;
+import io.trino.spi.expression.Call;
+import io.trino.spi.expression.ConnectorExpression;
+import io.trino.spi.expression.Constant;
+import io.trino.spi.expression.FunctionName;
+import io.trino.spi.expression.StandardFunctions;
+import io.trino.spi.expression.Variable;
+import io.trino.spi.predicate.Domain;
+import io.trino.spi.predicate.TupleDomain;
+import io.trino.spi.type.Type;
+import io.trino.spi.type.VarcharType;
+import org.testng.annotations.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static io.trino.spi.type.BooleanType.BOOLEAN;
+import static io.trino.type.InternalTypeManager.TESTING_TYPE_MANAGER;
+import static org.apache.paimon.fileindex.FileIndexCommon.toMapKey;
+import static
org.apache.paimon.trino.TrinoFilterExtractor.TRINO_MAP_ELEMENT_AT_FUNCTION_NAME;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** The test of TestTrinoFilterExtractor. */
+public class TestTrinoFilterExtractor {
+
+ @Test
+ public void testExtractTrinoColumnHandleForExpressionFilter() {
+ TupleDomain<ColumnHandle> summary = TupleDomain.all();
+ Type mapType =
TESTING_TYPE_MANAGER.fromSqlType("map<varchar,varchar>");
+ String columnName = "map";
+ String mapKeyName = "key";
+ String constantValue = "value";
+ Slice value = Slices.utf8Slice(constantValue);
+ Call elemetAtFuntion =
+ new Call(
+ BOOLEAN,
+ new FunctionName(TRINO_MAP_ELEMENT_AT_FUNCTION_NAME),
+ List.of(
+ new Variable(columnName, mapType),
+ new Constant(
+
io.airlift.slice.Slices.utf8Slice(mapKeyName),
+
VarcharType.createUnboundedVarcharType())));
+ ConnectorExpression expression =
+ new Call(
+ BOOLEAN,
+ StandardFunctions.EQUAL_OPERATOR_FUNCTION_NAME,
+ List.of(
+ elemetAtFuntion,
+ new Constant(value,
VarcharType.createUnboundedVarcharType())));
+ Map<String, ColumnHandle> assignments = Maps.newHashMap();
+ assignments.put(
+ columnName,
+ TrinoColumnHandle.of(
+ columnName, DataTypes.MAP(DataTypes.STRING(),
DataTypes.STRING())));
+ Constraint constraint = new Constraint(summary, expression,
assignments);
+ Map<TrinoColumnHandle, Domain> domainMap =
+
TrinoFilterExtractor.extractTrinoColumnHandleForExpressionFilter(constraint);
+ assertThat(domainMap.entrySet().size()).isEqualTo(1);
+ Map.Entry<TrinoColumnHandle, Domain> next =
domainMap.entrySet().iterator().next();
+
assertThat(next.getKey().getColumnName()).isEqualTo(toMapKey(columnName,
mapKeyName));
+ assertThat(
+ next.getValue()
+ .getValues()
+ .getRanges()
+ .getOrderedRanges()
+ .get(0)
+ .getLowBoundedValue())
+ .isEqualTo(value);
+ }
+}
diff --git a/pom.xml b/pom.xml
index 9cec154..4bd3084 100644
--- a/pom.xml
+++ b/pom.xml
@@ -261,6 +261,20 @@ under the License.
</configuration>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <version>3.2.1</version>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
@@ -518,4 +532,4 @@ under the License.
</pluginManagement>
</build>
-</project>
\ No newline at end of file
+</project>