You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2025/12/29 19:42:48 UTC
(sis) 02/03: When optimizing a query for a `FeatureType`, need to take in account that subtypes may exist. It forces us to consider all possible sub-types if they are known, or be conservative otherwise.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit a3a0d404ad8046cf9ef132561efc9912a88e3125
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Dec 26 19:41:57 2025 +0100
When optimizing a query for a `FeatureType`, need to take in account that subtypes may exist.
It forces us to consider all possible sub-types if they are known, or be conservative otherwise.
---
.../main/org/apache/sis/feature/Features.java | 4 +
.../internal/shared/FeatureProjectionBuilder.java | 5 +-
.../org/apache/sis/filter/AssociationValue.java | 73 +++++---
.../apache/sis/filter/BinaryGeometryFilter.java | 23 +--
.../org/apache/sis/filter/IdentifierFilter.java | 16 +-
.../main/org/apache/sis/filter/Optimization.java | 196 ++++++++++++++++++++-
.../main/org/apache/sis/filter/PropertyValue.java | 101 ++++++-----
.../org/apache/sis/filter/sqlmm/TwoGeometries.java | 30 +---
.../apache/sis/geometry/wrapper/Geometries.java | 13 ++
.../sis/geometry/wrapper/jts/FilteringContext.java | 2 +-
.../org/apache/sis/filter/LogicalFilterTest.java | 2 +-
.../apache/sis/filter/sqlmm/RegistryTestCase.java | 2 +-
.../sis/storage/netcdf/base/DiscreteSampling.java | 13 ++
.../sis/storage/sql/feature/FeatureStream.java | 2 +-
.../org/apache/sis/storage/sql/feature/Table.java | 11 ++
.../sql/feature/SelectionClauseWriterTest.java | 2 +-
.../org/apache/sis/storage/AbstractFeatureSet.java | 31 +++-
.../main/org/apache/sis/storage/FeatureQuery.java | 21 ++-
.../main/org/apache/sis/storage/FeatureSubset.java | 17 ++
.../org/apache/sis/storage/MemoryFeatureSet.java | 135 ++++++++++++--
.../storage/aggregate/ConcatenatedFeatureSet.java | 25 ++-
.../sis/storage/aggregate/JoinFeatureSet.java | 16 +-
.../org/apache/sis/storage/internal/Resources.java | 5 +
.../sis/storage/internal/Resources.properties | 1 +
.../sis/storage/internal/Resources_fr.properties | 1 +
.../main/org/apache/sis/util/Classes.java | 12 +-
.../test/org/apache/sis/map/SEPortrayerTest.java | 2 +-
.../sis/storage/shapefile/ShapefileStore.java | 14 +-
.../org/apache/sis/storage/gdal/FeatureLayer.java | 12 ++
29 files changed, 635 insertions(+), 152 deletions(-)
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
index dc27495cde..429a40f3da 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
@@ -26,6 +26,7 @@ import org.opengis.metadata.quality.ConformanceResult;
import org.opengis.metadata.quality.DataQuality;
import org.opengis.metadata.quality.Element;
import org.opengis.metadata.quality.Result;
+import org.apache.sis.util.OptionalCandidate;
import org.apache.sis.util.iso.Names;
import org.apache.sis.util.iso.DefaultNameFactory;
import org.apache.sis.feature.internal.Resources;
@@ -209,6 +210,7 @@ public final class Features {
*
* @since 1.0
*/
+ @OptionalCandidate
public static FeatureType findCommonParent(final Iterable<? extends FeatureType> types) {
return (types != null) ? CommonParentFinder.select(types) : null;
}
@@ -240,6 +242,7 @@ public final class Features {
*
* @since 1.0
*/
+ @OptionalCandidate
public static Class<?> getValueClass(PropertyType type) {
while (type instanceof Operation) {
final IdentifiedType result = ((Operation) type).getResult();
@@ -281,6 +284,7 @@ public final class Features {
*
* @since 0.8
*/
+ @OptionalCandidate
public static GenericName getValueTypeName(final PropertyType property) {
if (property instanceof FeatureAssociationRole) {
// Tested first because this is the main interest for this method.
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java
index 17ca838849..3f98d67641 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java
@@ -213,7 +213,6 @@ public final class FeatureProjectionBuilder extends FeatureTypeBuilder {
* In such case, the caller does not want the operation to be executed, since the property will rather
* be used as a slot for receiving the result.
*
- * @param childType the feature type to use.
* @param expression the expression from which to get the expected type.
* @return handler for the property, or {@code null} if it cannot be resolved.
*/
@@ -464,6 +463,8 @@ public final class FeatureProjectionBuilder extends FeatureTypeBuilder {
/**
* Returns a string representation for debugging purposes.
+ *
+ * @return a string representation for debugging purposes.
*/
@Override
public String toString() {
@@ -757,7 +758,7 @@ public final class FeatureProjectionBuilder extends FeatureTypeBuilder {
*/
public Optional<FeatureProjection> project() {
final var optimizer = new Optimization();
- optimizer.setFeatureType(source);
+ optimizer.setFinalFeatureType(source);
requested.forEach((item) -> item.finish(optimizer));
/*
* Add properties for all dependencies that are required by operations but are not already present.
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
index 8db223d154..bf68949e9f 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.filter;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Set;
import java.util.List;
@@ -156,42 +157,60 @@ walk: if (instance != null) {
/**
* If at least one evaluated property is a link, replaces this expression by more direct references
- * to the target properties. This is needed for better SQL WHERE clause in database queries.
+ * to the target properties. This is needed for better SQL {@code WHERE} clause in database queries.
*/
@Override
public Expression<Feature, V> optimize(final Optimization optimization) {
- final FeatureType specifiedType = optimization.getFeatureType();
-walk: if (specifiedType != null) try {
- FeatureType type = specifiedType;
- String[] direct = path; // To be cloned before any modification.
- for (int i=0; i<path.length; i++) {
- PropertyType property = type.getProperty(path[i]);
- Optional<String> link = Features.getLinkTarget(property);
- if (link.isPresent()) {
- if (direct == path) direct = direct.clone();
- property = type.getProperty(direct[i] = link.get());
+ final var actualTypes = new ArrayList<FeatureType>();
+ final var pathInstances = new ArrayList<String[]>(2);
+ pathInstances.add(path);
+ try {
+ final String[] actualPath = optimization.constantResultForAllTypes((type) -> {
+ String[] realPath = path; // To be cloned before any modification.
+ for (int i=0; i<realPath.length; i++) {
+ PropertyType property = type.getProperty(realPath[i]);
+ Optional<String> link = Features.getLinkTarget(property);
+ if (link.isPresent()) {
+ if (realPath == path) realPath = realPath.clone();
+ property = type.getProperty(realPath[i] = link.get());
+ }
+ if (property instanceof FeatureAssociationRole) {
+ type = ((FeatureAssociationRole) property).getValueType();
+ } else {
+ return null;
+ }
}
- if (!(property instanceof FeatureAssociationRole)) break walk;
- type = ((FeatureAssociationRole) property).getValueType();
- }
+ actualTypes.add(type);
+ // Workaround needed because the `equals(…)` method of arrays performs an identity comparison.
+ for (final String[] canonical : pathInstances) {
+ if (Arrays.equals(canonical, realPath)) {
+ return canonical;
+ }
+ }
+ pathInstances.add(realPath);
+ return realPath;
+ });
/*
* At this point all links have been resolved, up to the final property to evaluate.
* Delegate the final property optimization to `accessor` which may not only resolve
* links but also tune the `ObjectConverter`.
*/
- final Expression<Feature, V> converted;
- optimization.setFeatureType(type);
- try {
- converted = accessor.optimize(optimization);
- } finally {
- optimization.setFeatureType(specifiedType);
- }
- if (converted != accessor || direct != path) {
- if (converted instanceof PropertyValue<?>) {
- return new AssociationValue<>(direct, (PropertyValue<V>) converted);
- } else {
- // If not a `PropertyValue`, then it should be a `Literal`.
- return converted;
+ if (actualPath != null) {
+ final Expression<Feature, V> converted;
+ final Set<FeatureType> currentTypes = optimization.getFinalFeatureTypes();
+ optimization.setFinalFeatureTypes(actualTypes);
+ try {
+ converted = accessor.optimize(optimization);
+ } finally {
+ optimization.setFinalFeatureTypes(currentTypes);
+ }
+ if (converted != accessor || actualPath != path) {
+ if (converted instanceof PropertyValue<?>) {
+ return new AssociationValue<>(actualPath, (PropertyValue<V>) converted);
+ } else {
+ // If not a `PropertyValue`, then it should be a `Literal`.
+ return converted;
+ }
}
}
} catch (PropertyNotFoundException e) {
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
index 8df570ae0c..b8b50751c0 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
@@ -18,12 +18,10 @@ package org.apache.sis.filter;
import java.util.List;
import javax.measure.Unit;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.geometry.wrapper.Geometries;
import org.apache.sis.geometry.wrapper.GeometryWrapper;
import org.apache.sis.geometry.wrapper.SpatialOperationContext;
-import org.apache.sis.feature.internal.shared.AttributeConvention;
import org.apache.sis.filter.base.Node;
import org.apache.sis.util.Exceptions;
@@ -31,11 +29,9 @@ import org.apache.sis.util.Exceptions;
import org.opengis.filter.Filter;
import org.opengis.filter.Literal;
import org.opengis.filter.Expression;
-import org.opengis.filter.ValueReference;
import org.opengis.filter.SpatialOperator;
import org.opengis.filter.BinarySpatialOperator;
import org.opengis.filter.InvalidFilterValueException;
-import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyNotFoundException;
@@ -218,18 +214,13 @@ abstract class BinaryGeometryFilter<R> extends Node implements SpatialOperator<R
* then try to fetch the CRS of the property values. If we can transform the literal to that
* CRS, do it now in order to avoid doing this transformation for all feature instances.
*/
- final FeatureType featureType = optimization.getFeatureType();
- if (featureType != null && other instanceof ValueReference<?,?>) try {
- final CoordinateReferenceSystem targetCRS = AttributeConvention.getCRSCharacteristic(
- featureType, featureType.getProperty(((ValueReference<?,?>) other).getXPath()));
- if (targetCRS != null) {
- final GeometryWrapper geometry = wrapper.apply(null);
- final GeometryWrapper transformed = geometry.transform(targetCRS);
- if (geometry != transformed) {
- literal = Optimization.literal(transformed);
- if (literal == effective1) effective1 = literal;
- else effective2 = literal;
- }
+ final GeometryWrapper geometry = wrapper.apply(null);
+ if (geometry != null) try {
+ final GeometryWrapper transformed = geometry.transform(optimization.findExpectedCRS(other).orElse(null));
+ if (geometry != transformed) {
+ literal = Optimization.literal(transformed);
+ if (literal == effective1) effective1 = literal;
+ else effective2 = literal;
}
} catch (PropertyNotFoundException | TransformException e) {
warning(e, true);
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
index c3439df00c..80b3ecb877 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
@@ -17,16 +17,15 @@
package org.apache.sis.filter;
import java.util.List;
+import java.util.HashSet;
import java.util.Collection;
import java.util.Objects;
import org.apache.sis.filter.base.Node;
import org.apache.sis.filter.base.XPathSource;
-import org.apache.sis.feature.Features;
import org.apache.sis.feature.internal.shared.AttributeConvention;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyNotFoundException;
import org.opengis.filter.Expression;
import org.opengis.filter.ResourceId;
@@ -84,12 +83,17 @@ final class IdentifierFilter extends Node
*/
@Override
public Filter<Feature> optimize(Optimization optimization) {
- final FeatureType type = optimization.getFeatureType();
- if (type != null) try {
- return Features.getLinkTarget(type.getProperty(property)).map((n) -> new IdentifierFilter(this, n)).orElse(this);
+ final var found = new HashSet<String>();
+ try {
+ final String preferredName = optimization.getPreferredPropertyName(property, found);
+ if (!preferredName.equals(property)) {
+ return new IdentifierFilter(this, preferredName);
+ }
} catch (PropertyNotFoundException e) {
warning(e, true);
- return Filter.exclude();
+ if (found.isEmpty()) {
+ return Filter.exclude(); // The property does not exist in any feature type.
+ }
}
return this;
}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
index 3253ba51be..4b796616a9 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java
@@ -19,10 +19,19 @@ package org.apache.sis.filter;
import java.util.Map;
import java.util.Set;
import java.util.List;
+import java.util.Iterator;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.ConcurrentModificationException;
+import java.util.Optional;
+import java.util.function.Function;
import java.util.function.Predicate;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.feature.internal.shared.AttributeConvention;
+import org.apache.sis.feature.Features;
+import org.apache.sis.geometry.wrapper.Geometries;
import org.apache.sis.math.FunctionProperty;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.Containers;
@@ -33,9 +42,11 @@ import org.opengis.util.CodeList;
import org.opengis.filter.Filter;
import org.opengis.filter.Literal;
import org.opengis.filter.Expression;
+import org.opengis.filter.ValueReference;
import org.opengis.filter.LogicalOperator;
import org.opengis.filter.LogicalOperatorName;
import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyNotFoundException;
/**
@@ -89,13 +100,15 @@ public class Optimization {
private static final Object COMPUTING = Void.TYPE;
/**
- * The type of feature instances to be filtered, or {@code null} if unknown.
+ * Exhaustive set of types of all feature instances that the filters and expressions may see.
+ * This is an empty set if the feature types are unknown or irrelevant for the type of resources to be filtered.
*/
- private FeatureType featureType;
+ private Set<FeatureType> featureTypes;
/**
* Filters and expressions already optimized. Also used for avoiding never-ending loops.
- * The map is created when first needed.
+ * The map is created when first needed. The null value (not the same as an empty map) is
+ * used for identifying the start of recursive invocations of {@code apply(…)} methods.
*
* <h4>Implementation note</h4>
* The same map is used for filters and expressions.
@@ -103,13 +116,17 @@ public class Optimization {
* If it happens anyway, it should still be okay because the method signatures are
* the same in both interfaces (only the return type changes), so the same methods
* would be invoked no matter if we consider the keys as a filter or an expression.
+ *
+ * @see #apply(Filter)
+ * @see #apply(Expression)
*/
- private Map<Object,Object> done;
+ private Map<Object, Object> done;
/**
* Creates a new instance.
*/
public Optimization() {
+ featureTypes = Set.of();
}
/**
@@ -118,9 +135,12 @@ public class Optimization {
* The default value is {@code null}.
*
* @return the type of feature instances to be filtered, or {@code null} if unknown.
+ *
+ * @deprecated Replaced by {@link #getFinalFeatureTypes()}.
*/
+ @Deprecated(since = "1.6", forRemoval = true)
public FeatureType getFeatureType() {
- return featureType;
+ return Containers.peekIfSingleton(getFinalFeatureTypes());
}
/**
@@ -130,9 +150,173 @@ public class Optimization {
* in advance.
*
* @param type the type of feature instances to be filtered, or {@code null} if unknown.
+ *
+ * @deprecated Replaced by {@link #setFinalFeatureTypes(Collection)}.
*/
+ @Deprecated(since = "1.6", forRemoval = true)
public void setFeatureType(final FeatureType type) {
- featureType = type;
+ setFinalFeatureType(type);
+ }
+
+ /**
+ * Returns the exhaustive set of the types of all feature instances that the filters and expressions may see.
+ * The super-types should not be included in the set, unless some features may be instances of these specific
+ * super-types rather than instances of a some sub-type. If the set of feature types is unknown or irrelevant
+ * for the type of resources to be filtered, then this method returns an empty set.
+ *
+ * <h4>Purpose</h4>
+ * A {@link org.apache.sis.storage.DataStore} may contain a hierarchy of feature types instead of a single type.
+ * A property may be absent in the parent type but present in some sub-types, or may be overridden in sub-types.
+ * If an optimization wants to evaluate once and for all an expression with literal parameters, the optimization
+ * needs to verify that the parameters are really literals in all possible sub-types.
+ *
+ * @return exhaustive set of types of all feature instances that the filters and expressions may see.
+ *
+ * @since 1.6
+ */
+ @SuppressWarnings("ReturnOfCollectionOrArrayField")
+ public Set<FeatureType> getFinalFeatureTypes() {
+ return featureTypes;
+ }
+
+ /**
+ * Specifies the exhaustive set of the types of all feature instances that the filters and expressions may see.
+ * The given collection should not include super-types, unless some features may be instances of these specific
+ * super-types rather than instances of a some sub-type. An empty collection means that feature types are unknown
+ * or irrelevant for the type of resources to be filtered.
+ *
+ * @param types exhaustive set of types of all feature instances that the filters and expressions may see.
+ *
+ * @since 1.6
+ */
+ public void setFinalFeatureTypes(final Collection<? extends FeatureType> types) {
+ featureTypes = Set.copyOf(types);
+ }
+
+ /**
+ * Specifies the single type of all feature instances that the filters and expressions may see.
+ * This is a convenience method delegating to {@link #setFinalFeatureTypes(Collection)} with a
+ * singleton or empty set.
+ *
+ * <p>Note that the given type should be effectively final, i.e. no subtype should exist.
+ * If the feature instances may be of some subtypes, then all subtypes should be enumerated
+ * in a call to {@link #setFinalFeatureTypes(Collection)}.</p>
+ *
+ * @param type the type of feature instances to be filtered, or {@code null} if unknown.
+ *
+ * @since 1.6
+ */
+ public final void setFinalFeatureType(final FeatureType type) {
+ setFinalFeatureTypes((type != null) ? Set.of(type) : Set.of());
+ }
+
+ /**
+ * If the result of applying the given function is equal for all feature types, returns that value.
+ * Otherwise, returns {@code null}. This is used for implementation of {@code optimize(…)} methods.
+ *
+ * @param <R> type of the result.
+ * @param mapper the operation to apply on each feature type.
+ * @return the constant result, or {@code null} if none.
+ */
+ final <R> R constantResultForAllTypes(final Function<FeatureType, R> mapper) {
+ final Iterator<FeatureType> it = getFinalFeatureTypes().iterator();
+ if (it.hasNext()) {
+ final R value = mapper.apply(it.next());
+ if (value != null) {
+ while (it.hasNext()) {
+ if (!value.equals(mapper.apply(it.next()))) {
+ return null;
+ }
+ }
+ return value;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Fetches the real name of the given property after resolution of links in all feature types.
+ * The real name depends on the feature types declared by {@link #getFinalFeatureTypes()}.
+ * If the specified property is present in all declared feature types and all these properties
+ * are links referencing the same target property, then this method returns that target property.
+ * Otherwise, this method returns {@code property}.
+ *
+ * <p>If at least one feature type does not have the requested property, then an exception is thrown.
+ * This method finished the iteration over all types before to throw {@link PropertyNotFoundException}.
+ * Therefore, the size of {@code addTo} can be used for detecting if at least one feature type has the
+ * property. If {@code addTo} is empty, then the property has not been found in any feature type.</p>
+ *
+ * @param property name of the property to resolve.
+ * @param addTo where to add the XPath of the specified property for all feature types.
+ * @return preferred name to use for fetching the property values for all feature types.
+ * @throws PropertyNotFoundException if at least one feature type does not have the specified property.
+ */
+ @SuppressWarnings("ThrowableResultIgnored")
+ final String getPreferredPropertyName(final String property, final Set<String> addTo) throws PropertyNotFoundException {
+ final var exceptions = new HashMap<FeatureType, PropertyNotFoundException>();
+ for (final FeatureType type : getFinalFeatureTypes()) {
+ try {
+ addTo.add(Features.getLinkTarget(type.getProperty(property)).orElse(property));
+ } catch (PropertyNotFoundException e) {
+ exceptions.putIfAbsent(type, e);
+ }
+ }
+ if (exceptions.isEmpty()) {
+ final String name = Containers.peekIfSingleton(addTo);
+ return (name != null) ? name : property;
+ }
+ /*
+ * Throws the exception associated with the most basic feature type.
+ * The base type search is not mandatory, but provide more useful stack trace.
+ */
+ final PropertyNotFoundException e = valueOfBaseType(exceptions);
+ while (!exceptions.isEmpty()) {
+ e.addSuppressed(valueOfBaseType(exceptions));
+ }
+ throw e;
+ }
+
+ /**
+ * Returns the value associated to the base type among all keys of the given map.
+ * If no base type is found, then an arbitrary entry is used.
+ * This method always removes exactly one entry from the map.
+ */
+ private static <E> E valueOfBaseType(final Map<FeatureType, E> map) {
+ E e = map.remove(Features.findCommonParent(map.keySet()));
+ if (e == null) {
+ Iterator<E> it = map.values().iterator();
+ e = it.next();
+ it.remove();
+ }
+ return e;
+ }
+
+ /**
+ * If the specified parameter should always use the same Coordinate Reference System, returns that <abbr>CRS</abbr>.
+ * The {@code parameter} argument is usually one of the elements returned by {@link Expression#getParameters()} or
+ * {@link Filter#getExpressions()}, and the <abbr>CRS</abbr> used by that parameter may depend on the feature types
+ * declared by {@link #getFinalFeatureTypes()}.
+ * The returned value is empty if the <abbr>CRS</abbr> is unknown or not the same for all feature types.
+ *
+ * @param parameter a parameter of a filter or expression.
+ * @return the <abbr>CRS</abbr> expected for the specified parameter, or empty if unknown of not unique.
+ * @throws PropertyNotFoundException if the parameter is a {@link ValueReference} and
+ * the referenced property has not been found in at least one feature type.
+ *
+ * @since 1.6
+ */
+ public Optional<CoordinateReferenceSystem> findExpectedCRS(final Expression<?,?> parameter)
+ throws PropertyNotFoundException
+ {
+ CoordinateReferenceSystem crs = null;
+ if (parameter instanceof Literal<?,?>) {
+ crs = Geometries.getCoordinateReferenceSystem(((Literal<?,?>) parameter).getValue());
+ } else if (parameter instanceof ValueReference<?,?>) {
+ final String xpath = ((ValueReference<?,?>) parameter).getXPath();
+ crs = constantResultForAllTypes(
+ (type) -> AttributeConvention.getCRSCharacteristic(type, type.getProperty(xpath)));
+ }
+ return Optional.ofNullable(crs);
}
/**
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
index f3a521ddd1..b5e930790a 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
@@ -17,20 +17,22 @@
package org.apache.sis.filter;
import java.util.List;
+import java.util.HashSet;
+import java.util.HashMap;
import java.util.Optional;
import java.util.Collection;
import org.apache.sis.feature.Features;
import org.apache.sis.util.ObjectConverter;
import org.apache.sis.util.ObjectConverters;
import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.collection.Containers;
+import org.apache.sis.util.resources.Errors;
import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
import org.apache.sis.filter.base.XPath;
import org.apache.sis.filter.base.XPathSource;
-import org.apache.sis.util.resources.Errors;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyType;
import org.opengis.feature.AttributeType;
import org.opengis.feature.PropertyNotFoundException;
@@ -260,13 +262,17 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V>
*/
@Override
public Expression<Feature, Object> optimize(final Optimization optimization) {
- final FeatureType type = optimization.getFeatureType();
- if (type != null) try {
- return Features.getLinkTarget(type.getProperty(name))
- .map((rename) -> new AsObject(rename, isVirtual)).orElse(this);
+ final var found = new HashSet<String>();
+ try {
+ final String preferredName = optimization.getPreferredPropertyName(name, found);
+ if (!preferredName.equals(name)) {
+ return new AsObject(preferredName, isVirtual);
+ }
} catch (PropertyNotFoundException e) {
warning(e, true);
- return NULL();
+ if (found.isEmpty()) {
+ return NULL(); // The property does not exist in any feature type.
+ }
}
return this;
}
@@ -331,47 +337,58 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V>
*/
@Override
public final Expression<Feature, V> optimize(final Optimization optimization) {
- final FeatureType featureType = optimization.getFeatureType();
- if (featureType != null) try {
- /*
- * Resolve link (e.g. "sis:identifier" as a reference to the real identifier property).
- * This is important for allowing `SQLStore` to use the property in SQL WHERE statements.
- * If there is no renaming to apply (which is the usual case), then `rename` is null.
- */
- String rename = name;
- PropertyType property = featureType.getProperty(rename);
+ /*
+ * Resolve links (e.g. "sis:identifier" as a reference to the real identifier property).
+ * This is important for allowing `SQLStore` to use the property in SQL WHERE statements.
+ */
+ final var found = new HashSet<String>();
+ final String preferredName;
+ try {
+ preferredName = optimization.getPreferredPropertyName(name, found);
+ } catch (PropertyNotFoundException e) {
+ warning(e, true);
+ return found.isEmpty() ? NULL() : this;
+ }
+ /*
+ * Check if the same converter can be used for all feature types.
+ * This is guaranteed to be true if the requested property contains
+ * the same class of values in all feature types.
+ */
+ final ObjectConverter<?, ? extends V> converter;
+ final var actualTypes = new HashMap<Class<?>, ObjectConverter<?, ? extends V>>();
+ converter = optimization.constantResultForAllTypes((featureType) -> {
+ PropertyType property = featureType.getProperty(preferredName);
Optional<String> target = Features.getLinkTarget(property);
- if (target.isPresent()) try {
- rename = target.get();
- property = featureType.getProperty(rename);
- } catch (PropertyNotFoundException e) {
- warning(e, true);
- rename = name;
+ if (target.isPresent()) {
+ property = featureType.getProperty(target.get());
}
- /*
- * At this point we did our best effort for having the property as an attribute,
- * which allows us to get the expected type. If the type is not `Object`, we can
- * try to fetch a more specific converter than the default `Converted` one.
- */
- Class<?> source = getSourceClass();
- final Class<?> original = source;
+ final Class<?> source;
if (property instanceof AttributeType<?>) {
source = ((AttributeType<?>) property).getValueClass();
+ } else {
+ source = getSourceClass();
}
- if (!(rename.equals(name) && source.equals(original))) {
- if (source == Object.class) {
- return new Converted<>(type, rename, isVirtual);
- } else if (type.isAssignableFrom(source)) {
- return new Unsafe<>(source, type, rename, isVirtual);
- } else {
- return new CastedAndConverted<>(source, type, rename, isVirtual);
- }
- }
- } catch (PropertyNotFoundException e) {
- warning(e, true);
- return NULL();
+ return actualTypes.computeIfAbsent(source, (s) -> ObjectConverters.find(s, type));
+ });
+ /*
+ * Finished to collect the class of values in declared feature types. The `actualTypes` may contain more
+ * than one element if different feature types define the same property with values of different types,
+ * but a unique converter was nevertheless found.
+ */
+ Class<?> source = Containers.peekIfSingleton(actualTypes.keySet());
+ if (converter == null || (preferredName.equals(name) && (source == null || source == getSourceClass()))) {
+ return this;
+ }
+ if (source == null) {
+ source = converter.getSourceClass();
+ }
+ if (source == Object.class) {
+ return new Converted<>(type, preferredName, isVirtual);
+ } else if (type.isAssignableFrom(source)) {
+ return new Unsafe<>(source, type, preferredName, isVirtual);
+ } else {
+ return new CastedAndConverted<>(source, type, preferredName, isVirtual);
}
- return this;
}
/**
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
index 81b49eaf28..d66ebf656b 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
@@ -17,19 +17,15 @@
package org.apache.sis.filter.sqlmm;
import java.util.List;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.filter.Optimization;
-import org.apache.sis.feature.internal.shared.AttributeConvention;
import org.apache.sis.geometry.wrapper.Geometries;
import org.apache.sis.geometry.wrapper.GeometryWrapper;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyNotFoundException;
import org.opengis.filter.Expression;
import org.opengis.filter.Literal;
-import org.opengis.filter.ValueReference;
/**
@@ -78,23 +74,15 @@ class TwoGeometries<R> extends SpatialFunction<R> {
*/
@Override
public Expression<R,?> optimize(final Optimization optimization) {
- final FeatureType featureType = optimization.getFeatureType();
- if (featureType != null) {
- final Expression<R,?> p1 = unwrap(geometry1);
- if (p1 instanceof ValueReference<?,?> && unwrap(geometry2) instanceof Literal<?,?>) try {
- final CoordinateReferenceSystem targetCRS = AttributeConvention.getCRSCharacteristic(
- featureType, featureType.getProperty(((ValueReference<?,?>) p1).getXPath()));
- if (targetCRS != null) {
- final GeometryWrapper literal = geometry2.apply(null);
- if (literal != null) {
- final GeometryWrapper tr = literal.transform(targetCRS);
- if (tr != literal) {
- @SuppressWarnings({"unchecked","rawtypes"})
- final Expression<R,?>[] effective = getParameters().toArray(Expression[]::new);
- effective[1] = Optimization.literal(tr);
- return recreate(effective);
- }
- }
+ if (unwrap(geometry2) instanceof Literal<?,?>) {
+ final GeometryWrapper literal = geometry2.apply(null);
+ if (literal != null) try {
+ final GeometryWrapper transformed = literal.transform(optimization.findExpectedCRS(unwrap(geometry1)).orElse(null));
+ if (transformed != literal) {
+ @SuppressWarnings({"unchecked","rawtypes"})
+ final Expression<R,?>[] effective = getParameters().toArray(Expression[]::new);
+ effective[1] = Optimization.literal(transformed);
+ return recreate(effective);
}
} catch (PropertyNotFoundException | TransformException e) {
warning(e, true);
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
index 120a5a34ea..48e6b337cf 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
@@ -233,6 +233,19 @@ public abstract class Geometries<G> implements Serializable {
return wrapper.implementation();
}
+ /**
+ * Returns the coordinate reference system of the given geometry, or {@code null} if none.
+ * This is a convenience method for cases where the <abbr>CRS</abbr> is the only desired information.
+ * If more information are needed, use {@link #wrap(Object)} instead.
+ *
+ * @param geometry the geometry instance (can be {@code null}).
+ * @return the coordinate reference system, or {@code null}.
+ * @throws BackingStoreException if the operation failed because of a checked exception.
+ */
+ public static CoordinateReferenceSystem getCoordinateReferenceSystem(final Object geometry) {
+ return wrap(geometry).map(GeometryWrapper::getCoordinateReferenceSystem).orElse(null);
+ }
+
/**
* Wraps the geometry stored in a property of the given feature. This method should be used
* instead of {@link #wrap(Object)} when the value come from a feature instance in order to
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/FilteringContext.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/FilteringContext.java
index ecf02b8394..71a7c24206 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/FilteringContext.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/FilteringContext.java
@@ -32,7 +32,7 @@ import org.apache.sis.system.DelayedRunnable;
*
* <p>Ideally this object should be created when a filtering operation on a collection of features
* is about to start, and disposed after the filtering operation is completed. We do not yet have
- * a notification mechanism for those events, so current implementation use a {@link ThreadLocal}.
+ * a notification mechanism for those events, so current implementation use a {@link Queue}.
* A future version may revisit this strategy and expand the use of "filtering context" to all
* geometry implementations, not only JTS (but we may keep a specialized JTS subclass).</p>
*
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java
index e4dade8c0e..8106553156 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java
@@ -253,7 +253,7 @@ public final class LogicalFilterTest extends TestCase {
* Notify the optimizer that property values will be of `String` type.
* The optimizer should compute an `ObjectConverter` in advance.
*/
- optimization.setFeatureType(feature);
+ optimization.setFinalFeatureType(feature);
final var optimized = optimization.apply(expression);
assertEquals(200, expression.apply(instance).intValue());
assertNotSame(expression, optimized);
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java
index ab5ecd5c0f..533ff4b75c 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java
@@ -524,7 +524,7 @@ public abstract class RegistryTestCase<G> extends TestCaseWithLogs {
final var optimization = new Optimization();
final var ftb = new FeatureTypeBuilder();
ftb.addAttribute(library.pointClass).setName(P_NAME).setCRS(HardCodedCRS.WGS84);
- optimization.setFeatureType(ftb.setName("Test").build());
+ optimization.setFinalFeatureType(ftb.setName("Test").build());
final var optimized = optimization.apply(function);
assertNotSame(function, optimized, "Optimization should produce a new expression.");
/*
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/DiscreteSampling.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/DiscreteSampling.java
index 9d2d6dcf42..940dcd9e52 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/DiscreteSampling.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/DiscreteSampling.java
@@ -18,8 +18,11 @@ package org.apache.sis.storage.netcdf.base;
import org.apache.sis.setup.GeometryLibrary;
import org.apache.sis.geometry.wrapper.Geometries;
+import org.apache.sis.filter.Optimization;
import org.apache.sis.storage.DataStore;
import org.apache.sis.storage.AbstractFeatureSet;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.FeatureQuery;
import org.apache.sis.storage.base.StoreResource;
import org.apache.sis.storage.event.StoreListeners;
import org.apache.sis.util.resources.Errors;
@@ -86,6 +89,16 @@ public abstract class DiscreteSampling extends AbstractFeatureSet implements Sto
return lock;
}
+ /**
+ * Configures the optimization of a query with the knowledge that the feature type is final.
+ * This configuration asserts that all features will be instances of the type returned by
+ * {@link #getType()}, with no sub-type.
+ */
+ @Override
+ protected final void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException {
+ optimizer.setFinalFeatureType(getType());
+ }
+
/**
* Returns the error message for a file that cannot be read.
*
diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
index 77e62ca76a..8cd7d05a69 100644
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
@@ -196,7 +196,7 @@ final class FeatureStream extends DeferredStream<Feature> {
return execute(() -> {
Stream<Feature> stream = this;
final var optimization = new Optimization();
- optimization.setFeatureType(table.featureType);
+ optimization.setFinalFeatureType(table.featureType);
for (final var filter : optimization.applyAndDecompose((Filter<? super Feature>) predicate)) {
if (filter == Filter.include()) continue;
if (filter == Filter.exclude()) return empty();
diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
index c6a9fec04e..0588106972 100644
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
@@ -29,8 +29,10 @@ import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import org.opengis.util.GenericName;
import org.opengis.geometry.Envelope;
+import org.apache.sis.filter.Optimization;
import org.apache.sis.filter.InvalidXPathException;
import org.apache.sis.filter.base.XPath;
+import org.apache.sis.storage.FeatureQuery;
import org.apache.sis.storage.AbstractFeatureSet;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.InternalDataStoreException;
@@ -605,4 +607,13 @@ final class Table extends AbstractFeatureSet {
public Stream<Feature> features(final boolean parallel) {
return new FeatureStream(this, parallel);
}
+
+ /**
+ * Configures the optimization of a query with the knowledge that the feature type is final.
+ * This configuration asserts that all features will be instances of {@link #featureType} with no sub-type.
+ */
+ @Override
+ protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException {
+ optimizer.setFinalFeatureType(featureType);
+ }
}
diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java
index d9919b26ae..5e58eb45e5 100644
--- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java
+++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java
@@ -131,7 +131,7 @@ public final class SelectionClauseWriterTest extends TestCase implements SchemaM
Filter<Feature> filter = FF.intersects(FF.property("BETA"), FF.literal(bbox));
final var optimization = new Optimization();
- optimization.setFeatureType(table.featureType);
+ optimization.setFinalFeatureType(table.featureType);
verifySQL(optimization.apply(filter), "ST_Intersects(\"BETA\", " +
"ST_GeomFromText('POLYGON ((20 -10, 25 -10, 25 -5, 20 -5, 20 -10))'))");
}
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractFeatureSet.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractFeatureSet.java
index 102d8f179e..d9cf12ac8a 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractFeatureSet.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractFeatureSet.java
@@ -25,6 +25,7 @@ import org.opengis.metadata.Metadata;
import org.apache.sis.storage.event.StoreListeners;
import org.apache.sis.storage.base.MetadataBuilder;
import org.apache.sis.storage.base.WarningAdapter;
+import org.apache.sis.filter.Optimization;
import org.apache.sis.filter.base.WarningEvent;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -49,7 +50,7 @@ import org.opengis.feature.FeatureType;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.6
* @since 1.2
*/
public abstract class AbstractFeatureSet extends AbstractResource implements FeatureSet {
@@ -127,6 +128,32 @@ public abstract class AbstractFeatureSet extends AbstractResource implements Fea
}
}
+ /**
+ * Configures the optimization of a query to be applied on this {@code FeatureSet}.
+ * This method is invoked indirectly by the default implementation of {@link #subset(Query)}.
+ * The default implementation of this method does nothing.
+ *
+ * <h4>Recommendation</h4>
+ * Subclasses should override this method as below if they can guarantee that the feature type returned
+ * by {@link #getType()} is final, i.e. that the result of the query will not contain any feature which
+ * (before {@linkplain FeatureQuery#getProjection() projection}) is an instance of some subtype:
+ *
+ * {@snippet lang="java" :
+ * @Override
+ * protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException {
+ * optimizer.setFinalFeatureType(getType());
+ * }
+ * }
+ *
+ * @param query definition of feature and feature properties filtering applied at reading time.
+ * @param optimizer the optimization to configure.
+ * @throws DataStoreException if an error occurred during the configuration of the optimizer.
+ *
+ * @since 1.6
+ */
+ protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException {
+ }
+
/**
* Invoked in a synchronized block the first time that {@code getMetadata()} is invoked.
* The default implementation populates metadata based on information provided by
@@ -142,7 +169,7 @@ public abstract class AbstractFeatureSet extends AbstractResource implements Fea
*/
@Override
protected Metadata createMetadata() throws DataStoreException {
- final MetadataBuilder builder = new MetadataBuilder();
+ final var builder = new MetadataBuilder();
builder.addDefaultMetadata(this, listeners);
return builder.build();
}
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
index 9ee1019a4e..24a06a4ec4 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
@@ -83,7 +83,7 @@ import org.opengis.filter.SortProperty;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
* @since 1.1
*/
public class FeatureQuery extends Query implements Cloneable, Emptiable, Serializable {
@@ -641,6 +641,8 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali
* The literal, value reference or more complex expression to be retrieved by a {@code Query}.
* Never {@code null}.
*
+ * @return the expression (often a literal) for the value to retrieve.
+ *
* @since 1.5
*/
public Expression<? super Feature, ?> expression() {
@@ -650,6 +652,8 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali
/**
* The name to assign to the expression result, or {@code null} if unspecified.
*
+ * @return optional name for the expression result.
+ *
* @since 1.5
*/
public GenericName alias() {
@@ -662,6 +666,8 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali
* a feature {@link Operation}. The latter are commonly called "computed fields" and are equivalent
* to SQL {@code GENERATED ALWAYS} keyword for columns.
*
+ * @return the projection type (stored or computing).
+ *
* @since 1.5
*/
public ProjectionType type() {
@@ -841,12 +847,18 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali
* @throws DataStoreException if an error occurred during the optimization of this query.
*
* @since 1.5
+ *
+ * @deprecated Moved to {@link AbstractFeatureSet#prepareQueryOptimization(FeatureQuery, Optimization)}
+ * because experience suggests that this is the class that know best how to configure.
*/
+ @Deprecated(since = "1.6", forRemoval = true)
protected void optimize(final FeatureSet source) throws DataStoreException {
if (selection != null) {
- final var optimization = new Optimization();
- optimization.setFeatureType(source.getType());
- selection = optimization.apply(selection);
+ final var optimizer = new Optimization();
+ if (source instanceof AbstractFeatureSet) {
+ ((AbstractFeatureSet) source).prepareQueryOptimization(this, optimizer);
+ }
+ selection = optimizer.apply(selection);
}
}
@@ -856,7 +868,6 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali
* @return a clone of this query.
*
* @see #FeatureQuery(FeatureQuery)
- * @see #optimize(FeatureSet)
*/
@Override
public FeatureQuery clone() {
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
index da64b8dac7..df9e4468e4 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
@@ -19,6 +19,7 @@ package org.apache.sis.storage;
import java.util.OptionalLong;
import java.util.stream.Stream;
import org.opengis.metadata.Metadata;
+import org.apache.sis.filter.Optimization;
import org.apache.sis.feature.internal.shared.FeatureProjection;
import org.apache.sis.storage.base.MetadataBuilder;
import org.apache.sis.storage.base.StoreUtilities;
@@ -73,6 +74,22 @@ final class FeatureSubset extends AbstractFeatureSet {
this.query = query;
}
+ /**
+ * Configures the optimization of a query for the expected types of all feature instances.
+ * If this subset is the result of a projection (in <abbr>SQL</abbr> sense), then all features
+ * are instances of {@link #getType()}. Otherwise, features are instances of the same type as
+ * in the original {@linkplain #source}, therefore the same optimization as the source is applied.
+ */
+ @Override
+ protected void prepareQueryOptimization(final FeatureQuery query, final Optimization optimizer) throws DataStoreException {
+ final FeatureType type = getType();
+ if (projection != null) {
+ optimizer.setFinalFeatureType(type);
+ } else if (source instanceof AbstractFeatureSet) {
+ ((AbstractFeatureSet) source).prepareQueryOptimization(query, optimizer);
+ }
+ }
+
/**
* Creates metadata about this subset.
* It includes information about the complete feature set.
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/MemoryFeatureSet.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/MemoryFeatureSet.java
index a2d8297510..9503679751 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/MemoryFeatureSet.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/MemoryFeatureSet.java
@@ -16,10 +16,16 @@
*/
package org.apache.sis.storage;
+import java.util.Set;
+import java.util.HashSet;
import java.util.Objects;
import java.util.Collection;
import java.util.OptionalLong;
import java.util.stream.Stream;
+import org.apache.sis.feature.Features;
+import org.apache.sis.filter.Optimization;
+import org.apache.sis.storage.internal.Resources;
+import org.apache.sis.util.resources.Errors;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
@@ -36,6 +42,15 @@ import org.opengis.feature.FeatureType;
* or when the features are in memory anyway (for example, a computation result).
* It should generally not be used for large data sets read from files or databases.
*
+ * <h2>Mutability</h2>
+ * By default, the feature collection given at construction time is assumed stable.
+ * If the content of that collection is modified, then the {@link #refresh()} method
+ * should be invoked for rebuilding the {@linkplain #allTypes set of feature types}.
+ *
+ * <h2>Thread-safety</h2>
+ * This class is thread-safe if the collection given at construction time is thread-safe.
+ * Synchronizations use the lock returned by {@link #getSynchronizationLock()}.
+ *
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
*
@@ -43,43 +58,121 @@ import org.opengis.feature.FeatureType;
*/
public class MemoryFeatureSet extends AbstractFeatureSet {
/**
- * The type specified at construction time.
+ * The base type of all feature instances that the collection can contain.
+ * All elements of {@link #allTypes} must be assignable to this base type.
*
* @see #getType()
*/
- protected final FeatureType type;
+ protected final FeatureType baseType;
+
+ /**
+ * The types, including sub-types, of all feature instances found in the collection.
+ * This is often a singleton containing only {@link #baseType}, but it may also be a
+ * set without {@code baseType} if all features are instances of various subtypes.
+ *
+ * <p>This set is modifiable and is updated when {@link #refresh()} is invoked.
+ * This set should always contains at least one element.</p>
+ *
+ * @see #refresh()
+ * @see #prepareQueryOptimization(FeatureQuery, Optimization)
+ */
+ protected final Set<FeatureType> allTypes;
/**
* The features specified at construction time, potentially as a modifiable collection.
- * For all features in this collection, {@link Feature#getType()} shall be {@link #type}.
+ * For all feature instances in this collection, the value returned by {@link Feature#getType()}
+ * shall be {@link #baseType} or a subtype of {@code baseType}.
*
* @see #features(boolean)
*/
protected final Collection<Feature> features;
/**
- * Creates a new set of features stored in memory. It is caller responsibility to ensure that
- * <code>{@linkplain Feature#getType()} == type</code> for all elements in the given collection
- * (this is not verified by this constructor).
+ * Creates a new set of feature instances stored in memory.
+ * The base feature type is determined automatically from the given collection of features.
+ * The collection shall contain at least one element.
*
- * @param parent the parent resource, or {@code null} if none.
- * @param type the type of all features in the given collection.
- * @param features collection of stored features. This collection will not be copied.
+ * @param features collection of stored features. This collection will not be copied.
+ * @throws IllegalArgumentException if the given collection is empty or does not contain
+ * feature instances having a common parent type.
*/
- public MemoryFeatureSet(final Resource parent, final FeatureType type, final Collection<Feature> features) {
+ public MemoryFeatureSet(Collection<Feature> features) {
+ this(null, null, features);
+ }
+
+ /**
+ * Creates a new set of feature instances stored in memory with specified parent resource and base type.
+ * This constructor verifies that all feature instances are assignable to a base type.
+ * That base type can be either specified explicitly or inferred automatically.
+ *
+ * @param parent the parent resource, or {@code null} if none.
+ * @param baseType the base type of all features in the given collection, or {@code null} for automatic.
+ * @param features collection of stored features. This collection will not be copied.
+ * @throws IllegalArgumentException if {@code baseType} is null and cannot be determined from the feature instances,
+ * or if some feature instances are not assignable to {@code baseType}.
+ */
+ public MemoryFeatureSet(final Resource parent, final FeatureType baseType, final Collection<Feature> features) {
super(parent);
- this.type = Objects.requireNonNull(type);
this.features = Objects.requireNonNull(features);
+ allTypes = new HashSet<>();
+ if (baseType != null) {
+ verifyFeatureInstances(baseType, true);
+ } else {
+ features.forEach((instance) -> allTypes.add(instance.getType()));
+ if (allTypes.isEmpty()) {
+ throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "features"));
+ }
+ }
+ if ((this.baseType = Features.findCommonParent(allTypes)) == null) {
+ throw new IllegalArgumentException(Resources.format(Resources.Keys.NoCommonFeatureType));
+ }
+ }
+
+ /**
+ * Scans all feature instances for building the set of feature types.
+ * This method opportunistically verifies that all instances are assignable to {@code baseType}.
+ *
+ * @param baseType tentative value of {@link #baseType}, may be provided before {@link #baseType} is set.
+ * @param creating whether this method is invoked from the constructor.
+ */
+ private void verifyFeatureInstances(final FeatureType baseType, final boolean creating) {
+ for (final Feature feature : features) {
+ final FeatureType type = feature.getType();
+ if (allTypes.add(type) && !baseType.isAssignableFrom(type)) {
+ allTypes.clear();
+ String message = Resources.format(Resources.Keys.FeatureNotAssignableToBaseType_2, baseType.getName(), type.getName());
+ throw creating ? new IllegalArgumentException(message) : new IllegalStateException(message);
+ }
+ }
+ if (allTypes.isEmpty()) {
+ allTypes.add(baseType);
+ }
+ }
+
+ /**
+ * Notifies this {@code FeatureSet} that the elements in the collection of features have changed.
+ * This method re-verifies that all feature instances are assignable to the {@link #baseType} and
+ * rebuilds the set of all feature types.
+ *
+ * @throws IllegalStateException if some features are not instances of {@link #baseType}.
+ */
+ public void refresh() {
+ synchronized (getSynchronizationLock()) {
+ allTypes.clear();
+ verifyFeatureInstances(baseType, false);
+ }
}
/**
* Returns the type common to all feature instances in this set.
+ * By default, this type is determined at construction time and does
+ * not change even if the content of the feature collection is updated.
*
* @return a description of properties that are common to all features in this dataset.
*/
@Override
public FeatureType getType() {
- return type;
+ return baseType;
}
/**
@@ -103,6 +196,20 @@ public class MemoryFeatureSet extends AbstractFeatureSet {
return parallel ? features.parallelStream() : features.stream();
}
+ /**
+ * Configures the optimization of a query with information about the expected types of all feature instances.
+ * This method is invoked indirectly when a {@linkplain #subset feature subset} is created from a query.
+ * The optimization depends on the set of {@linkplain #allTypes all feature types} found in the collection.
+ *
+ * @since 1.6
+ */
+ @Override
+ protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException {
+ synchronized (getSynchronizationLock()) {
+ optimizer.setFinalFeatureTypes(allTypes);
+ }
+ }
+
/**
* Tests whether this memory feature set is wrapping the same feature instances as the given object.
* This method checks also that the listeners are equal.
@@ -114,7 +221,7 @@ public class MemoryFeatureSet extends AbstractFeatureSet {
public boolean equals(final Object obj) {
if (obj != null && obj.getClass() == getClass()) {
final var other = (MemoryFeatureSet) obj;
- return type.equals(other.type) &&
+ return baseType.equals(other.baseType) &&
features.equals(other.features) &&
listeners.equals(other.listeners);
}
@@ -128,6 +235,6 @@ public class MemoryFeatureSet extends AbstractFeatureSet {
*/
@Override
public int hashCode() {
- return type.hashCode() + 31 * features.hashCode() + 37 * listeners.hashCode();
+ return baseType.hashCode() + 31 * features.hashCode() + 37 * listeners.hashCode();
}
}
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java
index 6824d5e3d2..a52af88c8e 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java
@@ -18,14 +18,17 @@ package org.apache.sis.storage.aggregate;
import java.util.Arrays;
import java.util.List;
+import java.util.Set;
import java.util.Collection;
import java.util.OptionalLong;
import java.util.stream.Stream;
import org.apache.sis.feature.Features;
+import org.apache.sis.filter.Optimization;
import org.apache.sis.storage.FeatureSet;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.AbstractFeatureSet;
+import org.apache.sis.storage.FeatureQuery;
import org.apache.sis.storage.Query;
import org.apache.sis.storage.Resource;
import org.apache.sis.util.ArgumentChecks;
@@ -70,6 +73,13 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
*/
private final FeatureType commonType;
+ /**
+ * The types, including sub-types, of all feature instances.
+ * This is often a singleton containing only {@link #commonType}, but it may also be a
+ * set without {@code commonType} if all features are instances of various subtypes.
+ */
+ private final Set<FeatureType> allTypes;
+
/**
* Creates a new concatenated feature set with the same types as the given feature set,
* but different sources. This is used for creating {@linkplain #subset(Query) subsets}.
@@ -78,6 +88,7 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
super(original);
this.sources = Containers.viewAsUnmodifiableList(sources);
commonType = original.commonType;
+ allTypes = original.allTypes;
}
/**
@@ -101,7 +112,8 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
for (int i=0; i<types.length; i++) {
types[i] = sources[i].getType();
}
- commonType = Features.findCommonParent(Arrays.asList(types));
+ allTypes = Set.copyOf(Arrays.asList(types));
+ commonType = Features.findCommonParent(allTypes);
if (commonType == null) {
throw new DataStoreContentException(Resources.format(Resources.Keys.NoCommonFeatureType));
}
@@ -240,4 +252,15 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
}
return modified ? new ConcatenatedFeatureSet(subsets, this) : this;
}
+
+ /**
+ * Configures the optimization of a query with information about the expected types of all feature instances.
+ * This method is invoked indirectly when a {@linkplain #subset feature subset} is created from a query.
+ *
+ * @since 1.6
+ */
+ @Override
+ protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException {
+ optimizer.setFinalFeatureTypes(allTypes);
+ }
}
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/JoinFeatureSet.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/JoinFeatureSet.java
index df2843b599..9837755c4e 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/JoinFeatureSet.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/JoinFeatureSet.java
@@ -36,6 +36,7 @@ import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.filter.DefaultFilterFactory;
+import org.apache.sis.filter.Optimization;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
@@ -69,7 +70,7 @@ import org.opengis.filter.BinaryComparisonOperator;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.6
* @since 1.0
*/
public class JoinFeatureSet extends AggregatedFeatureSet {
@@ -314,6 +315,19 @@ public class JoinFeatureSet extends AggregatedFeatureSet {
return StreamSupport.stream(it, parallel).onClose(it);
}
+ /**
+ * Configures the optimization of a query with information about the expected type of all feature instances.
+ * The default implementation uses the knowledge that all features will be instances of exactly the type
+ * returned by {@link #getType()}, with no sub-type.
+ * Subclasses should override this method if they break this assertion.
+ *
+ * @since 1.6
+ */
+ @Override
+ protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException {
+ optimizer.setFinalFeatureType(getType());
+ }
+
/**
* Creates a new features containing an association to the two given features.
* The {@code main} feature cannot be null (this is not verified).
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
index 3ed3d3f62c..15b79f539f 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
@@ -240,6 +240,11 @@ public class Resources extends IndexedResourceBundle {
*/
public static final short FeatureAlreadyPresent_2 = 16;
+ /**
+ * Feature of type “{1}” is not assignable to base type “{0}”.
+ */
+ public static final short FeatureNotAssignableToBaseType_2 = 86;
+
/**
* Feature “{1}” has not been found in the “{0}” data store.
*/
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
index 3857d9333f..9c688036fb 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
@@ -55,6 +55,7 @@ DuplicatedQueryProperty_3 = Query property \u201c{0}\u201d is duplicated
ExcessiveHeaderSize_1 = Header in the \u201c{0}\u201d file is too large.
ExcessiveStringSize_3 = Character string in the \u201c{0}\u201d file is too long. The string has {2} characters while the limit is {1}.
FeatureAlreadyPresent_2 = A feature named \u201c{1}\u201d is already present in the \u201c{0}\u201d data store.
+FeatureNotAssignableToBaseType_2 = Feature of type \u201c{1}\u201d is not assignable to base type \u201c{0}\u201d.
FeatureNotFound_2 = Feature \u201c{1}\u201d has not been found in the \u201c{0}\u201d data store.
FileAlreadyExists_2 = A {1,choice,0#file|1#directory} already exists at \u201c{0}\u201d.
FileIsNotAResourceDirectory_1 = The \u201c{0}\u201d file is not a directory of resources.
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
index 764205bd8a..8d63ff1001 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
@@ -60,6 +60,7 @@ DuplicatedQueryProperty_3 = La propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f
ExcessiveHeaderSize_1 = L\u2019en-t\u00eate dans le fichier \u00ab\u202f{0}\u202f\u00bb est trop grand.
ExcessiveStringSize_3 = La cha\u00eene de caract\u00e8res dans le fichier \u00ab\u202f{0}\u202f\u00bb est trop longue. La cha\u00eene fait {2} caract\u00e8res alors que la limite est {1}.
FeatureAlreadyPresent_2 = Une entit\u00e9 nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb est d\u00e9j\u00e0 pr\u00e9sente dans les donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb.
+FeatureNotAssignableToBaseType_2 = L\u2019entit\u00e9 de type \u00ab\u202f{1}\u202f\u00bb n\u2019est pas assignable au type de base \u00ab\u202f{0}\u202f\u00bb.
FeatureNotFound_2 = L\u2019entit\u00e9 \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans les donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb.
FileAlreadyExists_2 = Un {1,choice,0#fichier|1#r\u00e9pertoire} existe d\u00e9j\u00e0 \u00e0 l\u2019emplacement \u00ab\u202f{0}\u202f\u00bb.
FileIsNotAResourceDirectory_1 = Le fichier \u00ab\u202f{0}\u202f\u00bb n\u2019est pas un r\u00e9pertoire de ressources.
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
index 60823765c4..1fc1bea51d 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
@@ -55,7 +55,7 @@ import org.apache.sis.pending.jdk.JDK19;
* </ul>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.6
* @since 0.3
*/
public final class Classes {
@@ -355,7 +355,10 @@ public final class Classes {
* @param <T> the base type of elements in the given collection.
* @param objects the collection of objects.
* @return the set of classes of all objects in the given collection.
+ *
+ * @deprecated To be removed after removal of public deprecated methods.
*/
+ @Deprecated(since = "1.6", forRemoval = true)
private static <T> Set<Class<? extends T>> getClasses(final Iterable<? extends T> objects) {
final Set<Class<? extends T>> types = new LinkedHashSet<>();
for (final T object : objects) {
@@ -548,7 +551,10 @@ next: for (final Class<?> candidate : candidates) {
* @param objects a collection of objects. May contains duplicated values and null values.
* @return the most specialized class, or {@code null} if the given collection does not contain
* at least one non-null element.
+ *
+ * @deprecated This method is confusing as it works on instances instead of classes.
*/
+ @Deprecated(since = "1.6", forRemoval = true)
public static Class<?> findSpecializedClass(final Iterable<?> objects) {
final Set<Class<?>> types = getClasses(objects);
types.remove(null);
@@ -600,7 +606,11 @@ next: for (final Class<?> candidate : candidates) {
* @param objects a collection of objects. May contains duplicated values and null values.
* @return the most specific class common to all supplied objects, or {@code null} if the
* given collection does not contain at least one non-null element.
+ *
+ * @deprecated This method is confusing as it works on instances while {@link #findCommonClass(Class, Class)}
+ * works on classes.
*/
+ @Deprecated(since = "1.6", forRemoval = true)
public static Class<?> findCommonClass(final Iterable<?> objects) {
final Set<Class<?>> types = getClasses(objects);
types.remove(null);
diff --git a/incubator/src/org.apache.sis.portrayal.map/test/org/apache/sis/map/SEPortrayerTest.java b/incubator/src/org.apache.sis.portrayal.map/test/org/apache/sis/map/SEPortrayerTest.java
index c83d5795da..459b85eeb9 100644
--- a/incubator/src/org.apache.sis.portrayal.map/test/org/apache/sis/map/SEPortrayerTest.java
+++ b/incubator/src/org.apache.sis.portrayal.map/test/org/apache/sis/map/SEPortrayerTest.java
@@ -154,7 +154,7 @@ public class SEPortrayerTest {
shark1.setPropertyValue("specie", "White Shark");
shark1.setPropertyValue("length", 12.0);
- fishes = new MemoryFeatureSet(null, sharkType, List.of(fish1, fish2, shark1));
+ fishes = new MemoryFeatureSet(null, fishType, List.of(fish1, fish2, shark1));
final FeatureTypeBuilder boatbuilder = new FeatureTypeBuilder();
boatbuilder.setName("boat");
diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
index 9e0efb2357..bee856c6c8 100644
--- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
+++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
@@ -287,7 +287,7 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe
return featureSetView.getFileSet();
}
- private class AsFeatureSet extends AbstractFeatureSet implements WritableFeatureSet {
+ private final class AsFeatureSet extends AbstractFeatureSet implements WritableFeatureSet {
private final Rectangle2D.Double filter;
private final Set<String> dbfProperties;
@@ -325,6 +325,16 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe
return filter == null && dbfProperties == null && readShp;
}
+ /**
+ * Configures the optimization of a query with the knowledge that the feature type is final.
+ * This configuration asserts that all features will be instances of the type returned by
+ * {@link #getType()} with no sub-type.
+ */
+ @Override
+ protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException {
+ optimizer.setFinalFeatureType(getType());
+ }
+
@Override
public synchronized FeatureType getType() throws DataStoreException {
if (type == null) {
@@ -579,7 +589,7 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe
if (selection != null) {
//run optimizations
final Optimization optimization = new Optimization();
- optimization.setFeatureType(type);
+ optimization.setFinalFeatureType(type);
final ThreadLocal<Consumer<WarningEvent>> context = WarningEvent.LISTENER;
final Consumer<WarningEvent> old = context.get();
try {
diff --git a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java
index 0bbb5e6237..e517ec3fa4 100644
--- a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java
+++ b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java
@@ -30,12 +30,14 @@ import org.apache.sis.feature.builder.AttributeRole;
import org.apache.sis.feature.builder.AttributeTypeBuilder;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
import org.apache.sis.feature.internal.shared.AttributeConvention;
+import org.apache.sis.filter.Optimization;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.wrapper.Geometries;
import org.apache.sis.geometry.wrapper.GeometryType;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.storage.AbstractFeatureSet;
import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.FeatureQuery;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Workaround;
import org.apache.sis.util.resources.Errors;
@@ -68,6 +70,7 @@ final class FeatureLayer extends AbstractFeatureSet {
/**
* Description (list of fields) of all feature instances in this layer.
+ * All features will be instances of that type, with no sub-types.
*/
final FeatureType type;
@@ -326,6 +329,15 @@ final class FeatureLayer extends AbstractFeatureSet {
}
}
+ /**
+ * Configures the optimization of a query with the knowledge that the feature type is final.
+ * This configuration asserts that all features will be instances of {@link #type} with no sub-type.
+ */
+ @Override
+ protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException {
+ optimizer.setFinalFeatureType(type);
+ }
+
/**
* Returns the locale-dependent resources for error messages.
*/