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 <[email protected]>
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.
      */

Reply via email to