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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new f1393024bc When some filters implemented in pure Java are replaced by 
SQL expressions, remove the properties of `typeWithDependencies` that are not 
longer needed. It can reduce the number of columns requested in the `SELECT` 
statement.
f1393024bc is described below

commit f1393024bc65d62394c4abf663760ed937b99c69
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Nov 21 17:08:51 2025 +0100

    When some filters implemented in pure Java are replaced by SQL expressions,
    remove the properties of `typeWithDependencies` that are not longer needed.
    It can reduce the number of columns requested in the `SELECT` statement.
---
 .../apache/sis/feature/ExpressionOperation.java    |   2 +-
 .../org/apache/sis/feature/FeatureOperations.java  |  55 +++++-
 .../main/org/apache/sis/feature/Features.java      |  15 +-
 .../sis/feature/builder/FeatureTypeBuilder.java    |   8 +-
 .../feature/internal/shared/FeatureProjection.java | 208 ++++++++++++++++-----
 .../internal/shared/FeatureProjectionTest.java     |  79 ++++++--
 .../org/apache/sis/storage/sql/feature/Column.java |   2 +-
 .../sis/storage/sql/feature/FeatureStream.java     |   7 +-
 8 files changed, 299 insertions(+), 77 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
index c7f10889d8..a395f76219 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
@@ -68,7 +68,7 @@ final class ExpressionOperation<V> extends AbstractOperation {
      * The type of result of evaluating the expression.
      */
     @SuppressWarnings("serial")                         // Apache SIS 
implementations are serializable.
-    private final AttributeType<V> resultType;
+    final AttributeType<V> resultType;
 
     /**
      * The name of all feature properties that are known to be read by the 
expression.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
index 9890426a4e..d56a79c3d1 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
@@ -22,6 +22,7 @@ import org.opengis.util.GenericName;
 import org.opengis.util.FactoryException;
 import org.opengis.util.InternationalString;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.util.resources.Errors;
@@ -35,6 +36,7 @@ import org.opengis.feature.Feature;
 import org.opengis.feature.Operation;
 import org.opengis.feature.PropertyType;
 import org.opengis.feature.AttributeType;
+import org.opengis.feature.IdentifiedType;
 import org.opengis.feature.FeatureAssociationRole;
 import org.opengis.filter.Expression;
 
@@ -104,7 +106,7 @@ import org.opengis.filter.Expression;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.7
  */
 public final class FeatureOperations {
@@ -312,7 +314,7 @@ public final class FeatureOperations {
      *
      * @since 1.4
      */
-    public static <V> Operation function(final Map<String,?> identification,
+    public static <V> Operation function(final Map<String, ?> identification,
                                          final Function<? super Feature, ? 
extends V> expression,
                                          final AttributeType<? super V> 
resultType)
     {
@@ -341,13 +343,60 @@ public final class FeatureOperations {
      *
      * @since 1.4
      */
-    public static <V> Operation expression(final Map<String,?> identification,
+    public static <V> Operation expression(final Map<String, ?> identification,
                                            final Expression<? super Feature, 
?> expression,
                                            final AttributeType<V> resultType)
     {
         return function(identification, 
expression.toValueType(resultType.getValueClass()), resultType);
     }
 
+    /**
+     * Creates an operation with the same identification and result type than 
the given operation,
+     * but evaluated using the given expression. For example, if the given 
operation is the result
+     * of a previous call to {@link #expression expression(…)}, then invoking 
this method is equivalent
+     * to invoking {@code expression(…)} again with the same arguments except 
for {@code expression}.
+     *
+     * @param  operation   the operation to evaluate in a different way.
+     * @param  expression  the new expression to use for evaluating the 
operation.
+     * @return the new operation. May be the given operation if the expression 
is the same.
+     * @throws IllegalArgumentException if the {@linkplain 
Operation#getResult() result type}
+     *         of the given operation is not an {@link AttributeType}.
+     *
+     * @since 1.6
+     */
+    public static Operation replace(final PropertyType property, final 
Expression<? super Feature, ?> expression) {
+        final AttributeType<?> resultType;
+        if (property instanceof ExpressionOperation<?>) {
+            var operation = (ExpressionOperation) property;
+            if (operation.expression.equals(expression)) {
+                return operation;
+            }
+            resultType = operation.resultType;
+        } else if (property instanceof AttributeType<?>) {
+            resultType = (AttributeType<?>) property;
+        } else if (property instanceof Operation) {
+            final IdentifiedType type = ((Operation) property).getResult();
+            if (type instanceof AttributeType<?>) {
+                resultType = (AttributeType<?>) type;
+            } else {
+                throw 
illegalResultType(Errors.Keys.IllegalPropertyValueClass_3, property.getName(), 
AttributeType.class, type);
+            }
+        } else {
+            throw illegalResultType(Errors.Keys.IllegalArgumentClass_2, 
"property", property);
+        }
+        return expression(Map.of(AbstractIdentifiedType.INHERIT_FROM_KEY, 
property), expression, resultType);
+    }
+
+    /**
+     * Returns the exception to throw for an illegal result type.
+     * The last argument will be replaced by the class or interface of that 
argument.
+     */
+    private static IllegalArgumentException illegalResultType(final short key, 
final Object... arguments) {
+        final int last = arguments.length - 1;
+        arguments[last] = 
Classes.getStandardType(Classes.getClass(arguments[last]));
+        return new IllegalArgumentException(Errors.format(key, arguments));
+    }
+
     /**
      * Returns an expression for fetching the values of properties identified 
by the given type.
      * The returned expression will be the first of the following choices 
which is applicable:
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 d87f934aba..dc27495cde 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
@@ -29,6 +29,7 @@ import org.opengis.metadata.quality.Result;
 import org.apache.sis.util.iso.Names;
 import org.apache.sis.util.iso.DefaultNameFactory;
 import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.pending.jdk.JDK16;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.metadata.maintenance.ScopeCode;
@@ -49,7 +50,7 @@ import org.opengis.feature.PropertyType;
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.5
  */
 public final class Features {
@@ -212,6 +213,18 @@ public final class Features {
         return (types != null) ? CommonParentFinder.select(types) : null;
     }
 
+    /**
+     * Returns the name of all properties (including inherited properties) of 
the given feature type.
+     *
+     * @param  feature  the feature type from which to get the name of all 
properties.
+     * @return name of all properties of the specified feature type.
+     *
+     * @since 1.6
+     */
+    public static List<String> getPropertyNames(final FeatureType feature) {
+        return JDK16.toList(feature.getProperties(true).stream().map((p) -> 
p.getName().toString()));
+    }
+
     /**
      * Returns the type of values provided by the given property. For 
{@linkplain AttributeType attributes}
      * (which is the most common case), the value type is given by {@link 
AttributeType#getValueClass()}.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
index bfd7549cc7..7c997f3072 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
@@ -609,11 +609,11 @@ public class FeatureTypeBuilder extends TypeBuilder {
 
     /**
      * Returns a view of all attributes and associations added to the {@code 
FeatureType} to build.
-     * This list contains only properties declared explicitly to this builder;
-     * it does not include properties inherited from {@linkplain 
#getSuperTypes() super-types}.
+     * This list contains only properties declared explicitly to this builder.
+     * It does not include properties inherited from {@linkplain 
#getSuperTypes() super-types}.
      * The returned list is <em>live</em>: changes in this builder are 
reflected in that list and conversely.
-     * However, the returned list allows only {@linkplain List#remove(Object) 
remove} operations;
-     * new attributes or associations can be added only by calls to one of the 
{@code addAttribute(…)}
+     * However, the returned list allows only {@linkplain List#remove(Object) 
remove} operations.
+     * New attributes or associations can be added only by calls to one of the 
{@code addAttribute(…)}
      * or {@code addAssociation(…)} methods. Removal operations never affect 
the super-types.
      *
      * @return a live list over the properties declared to this builder.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
index deac884387..3d6dbc3552 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
@@ -19,6 +19,7 @@ package org.apache.sis.feature.internal.shared;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.LinkedHashMap;
 import java.util.Objects;
@@ -28,14 +29,24 @@ import java.util.function.UnaryOperator;
 import org.opengis.util.GenericName;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.CorruptedObjectException;
 import org.apache.sis.util.internal.shared.UnmodifiableArrayList;
+import org.apache.sis.pending.jdk.Record;
+import org.apache.sis.pending.jdk.JDK19;
+import org.apache.sis.feature.Features;
+import org.apache.sis.feature.FeatureOperations;
+import org.apache.sis.feature.AbstractOperation;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.feature.builder.PropertyTypeBuilder;
 import org.apache.sis.filter.visitor.ListingPropertyVisitor;
 import org.apache.sis.io.TableAppender;
 
 // 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.Operation;
 import org.opengis.filter.Expression;
 import org.opengis.filter.Literal;
@@ -50,7 +61,7 @@ import org.opengis.filter.ValueReference;
  * @author Guilhem Legal (Geomatys)
  * @author Martin Desruisseaux (Geomatys)
  */
-public final class FeatureProjection implements UnaryOperator<Feature> {
+public final class FeatureProjection extends Record implements 
UnaryOperator<Feature> {
     /**
      * The type of features with the properties explicitly requested by the 
user.
      * The property names may differ from the properties of the {@link 
FeatureProjectionBuilder#source() source}
@@ -72,7 +83,12 @@ public final class FeatureProjection implements 
UnaryOperator<Feature> {
      * The property names are the same as {@link #typeRequested} (i.e., may be 
aliases).
      * However, some operations may be wrapped in {@link OperationView}.
      *
-     * @see #dependencies()
+     * <p>This is <em>not</em> a container listing the properties of the 
source feature that are required.
+     * This type still assume that the {@link #apply(Feature)} method will 
receive complete source features.
+     * This type may differ from {@link #typeRequested} only when the latter 
contains operation that have not
+     * been converted to stored attributes.</p>
+     *
+     * @see #requiredSourceProperties()
      */
     public final FeatureType typeWithDependencies;
 
@@ -97,6 +113,28 @@ public final class FeatureProjection implements 
UnaryOperator<Feature> {
      */
     private final boolean createInstance;
 
+    /**
+     * Creates a new projection with the given properties.
+     *
+     * @param typeRequested         type of features with the properties 
explicitly requested by the user.
+     * @param typeWithDependencies  requested type augmented with dependencies 
required for the execution of operations.
+     * @param propertiesToCopy      names of the properties to be stored in 
the feature instances created by this object.
+     * @param expressions           expressions to apply on the source feature 
for fetching the property values.
+     * @param createInstance        whether the {@link #apply(Feature)} method 
shall create the feature instances.
+     */
+    private FeatureProjection(final FeatureType typeRequested,
+                              final FeatureType typeWithDependencies,
+                              final String[]    propertiesToCopy,
+                              final Expression<? super Feature, ?>[] 
expressions,
+                              final boolean createInstance)
+    {
+        this.typeRequested        = typeRequested;
+        this.typeWithDependencies = typeWithDependencies;
+        this.propertiesToCopy     = propertiesToCopy;
+        this.expressions          = expressions;
+        this.createInstance       = createInstance;
+    }
+
     /**
      * Creates a new projection with the given properties specified by a 
builder.
      * The {@link #apply(Feature)} method will copy the properties of the given
@@ -132,40 +170,17 @@ public final class FeatureProjection implements 
UnaryOperator<Feature> {
         this.expressions      = ArraysExt.resize(expressions, storedCount);
     }
 
-    /**
-     * Creates a new projection with a subset of the properties of another 
projection.
-     * This constructor is invoked when the caller handles itself some of the 
properties.
-     *
-     * <h4>Behavioral change</h4>
-     * Projections created by this constructor assumes that the feature 
instances given to the
-     * {@link #apply(Feature)} method are already instances of {@link 
#typeWithDependencies}
-     * and can be modified (if needed) in place. This constructor is designed 
for cases where
-     * the caller does itself a part of the {@code FeatureProjection} work.
-     *
-     * @param  parent     the projection from which to inherit the types and 
expressions.
-     * @param  remaining  index of the properties that still need to be copied 
after the caller did its processing.
-     *
-     * @see #forPreexistingFeatureInstances(int[])
-     */
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    private FeatureProjection(final FeatureProjection parent, final int[] 
remaining) {
-        createInstance       = false;
-        typeRequested        = parent.typeRequested;
-        typeWithDependencies = parent.typeWithDependencies;
-        expressions          = new Expression[remaining.length];
-        propertiesToCopy     = new String[remaining.length];
-        for (int i=0; i<remaining.length; i++) {
-            final int index = remaining[i];
-            propertiesToCopy[i] = parent.propertiesToCopy[index];
-            expressions[i] = parent.expressions[index];
-        }
-    }
-
     /**
      * Returns a variant of this projection where the caller has created the 
target feature instance itself.
      * The callers may have set some property values itself, and the {@code 
remaining} argument gives the
      * indexes of the properties that still need to be copied after caller's 
processing.
      *
+     * <h4>Recommendation</h4>
+     * Caller should ensure that the {@code remaining} array does not contain 
indexes <var>i</var>
+     * where {@code expressions[i]} is equivalent to {@code 
FilterFactory.property(propertiesToCopy[i])}
+     * because it would be a useless operation. This method does not perform 
that verification by itself
+     * on the assumption that it would duplicate work already done by the 
caller.
+     *
      * @param  remaining  index of the properties that still need to be copied 
after the caller did its processing.
      * @return a variant of this projection which only completes the 
projection done by the caller,
      *         or {@code null} if there is nothing left to complete.
@@ -174,34 +189,133 @@ public final class FeatureProjection implements 
UnaryOperator<Feature> {
         if (remaining.length == 0 && typeRequested == typeWithDependencies) {
             return null;
         }
-        return new FeatureProjection(this, remaining);
+        @SuppressWarnings({"unchecked", "rawtypes"})
+        final Expression<? super Feature, ?>[] filteredExpressions = new 
Expression[remaining.length];
+        final String[] filteredProperties = new String[remaining.length];
+        for (int i=0; i<remaining.length; i++) {
+            final int index = remaining[i];
+            filteredProperties [i] = propertiesToCopy[index];
+            filteredExpressions[i] = expressions[index];
+        }
+        return new FeatureProjection(typeRequested, typeWithDependencies, 
filteredProperties, filteredExpressions, false);
     }
 
     /**
      * Creates a new projection with the same properties as the source 
projection, but modified expressions.
-     * The modifications are specified by the given {@code mapper}. No 
expression can be added or removed.
-     * New expressions should return values of the same type as the previous 
expressions.
-     * The new expressions shall not introduce new dependencies.
+     * The modifications are specified by the given {@code mapper}, which 
should return values of the same
+     * type as the previous expressions. The new expressions shall not 
introduce new dependencies.
      *
      * <h4>Purpose</h4>
-     * This constructor is used when the caller can replace some expressions 
by <abbr>SQL</abbr> statements.
+     * This method is used when the caller can replace some expressions by 
<abbr>SQL</abbr> statements.
      *
-     * @param source  the projection to copy.
      * @param mapper  a function receiving in arguments the property name and 
the expression fetching the property value,
      *                and returning the expression to use in replacement of 
the function given in argument.
+     * @return the feature projection with modified expressions. May be {@code 
this} if there is no change.
      */
-    public FeatureProjection(final FeatureProjection source,
+    public FeatureProjection replaceExpressions(
             final BiFunction<String, Expression<? super Feature, ?>, 
Expression<? super Feature, ?>> mapper)
     {
-        typeRequested    = source.typeRequested;
-        createInstance   = source.createInstance;
-        propertiesToCopy = source.propertiesToCopy;
-        expressions      = source.expressions.clone();
+        final Map<String, Expression<? super Feature, ?>> filtered = 
JDK19.newLinkedHashMap(expressions.length);
         for (int i = 0; i < expressions.length; i++) {
-            expressions[i] = mapper.apply(propertiesToCopy[i], expressions[i]);
+            final String property = propertiesToCopy[i];
+            if (filtered.put(property, mapper.apply(property, expressions[i])) 
!= null) {
+                throw new 
CorruptedObjectException(Errors.format(Errors.Keys.DuplicatedElement_1, 
property));
+            }
+        }
+        /*
+         * The above loop replaced the expressions used for fetching values 
from the source feature instances
+         * and storing them as attributes. But expressions may also be used in 
an indirect way, as operations.
+         * Note that operations have no corresponding entries in the 
`propertiesToCopy` array.
+         */
+        final var builder = new FeatureTypeBuilder(typeWithDependencies);
+        for (final PropertyTypeBuilder property : 
builder.properties().toArray(PropertyTypeBuilder[]::new)) {
+            filtered.computeIfAbsent(property.getName().toString(), (name) -> {
+                final PropertyType type = property.build();
+                Expression<? super Feature, ?> expression = 
FeatureOperations.expressionOf(type);
+                if (!expression.equals(expression = mapper.apply(name, 
expression))) {
+                    
property.replaceBy(builder.addProperty(FeatureOperations.replace(type, 
expression)));
+                    return expression;
+                }
+                return null;    // No change, keep the current operation.
+            });
+        }
+        /*
+         * Some expressions may become unnecessary if the new expression only 
fetches a property value, then stores
+         * that value unchanged in the same feature instance. Note that we 
will need to remove only the expression,
+         * not the property in the `FeatureType`.
+         */
+        if (!createInstance) {
+            for (final var it = filtered.entrySet().iterator(); it.hasNext();) 
{
+                final var entry = it.next();
+                final var expression = entry.getValue();
+                if (expression instanceof ValueReference<?,?>) {
+                    final String name = ((ValueReference<?,?>) 
expression).getXPath();
+                    if (name.equals(entry.getKey())) {
+                        // The expression reads and stores a property of the 
same name in the same feature instance.
+                        final PropertyTypeBuilder property = 
builder.getProperty(name);
+                        if (property != null) {     // A null value would 
probably be a bug, but check anyway.
+                            property.remove();
+                            it.remove();
+                        }
+                    }
+                }
+            }
+        }
+        /*
+         * Maybe the new expressions have less dependencies than the old 
expressions. It happens if `mapper` replaced
+         * a pure Java filter by a SQL expression such as `SQRT(x*x + y*y)`, 
in which case the `x` and `y` properties
+         * are used by the database and do not need anymore to be forwarded to 
the Java code.
+         */
+        if (typeWithDependencies != typeRequested) {
+            final var unnecessary = new 
HashSet<>(Features.getPropertyNames(typeWithDependencies));
+            unnecessary.removeAll(Features.getPropertyNames(typeRequested));
+            for (var property : builder.properties()) {
+                final PropertyType type = property.build();
+                if (type instanceof AbstractOperation) {
+                    unnecessary.removeAll(((AbstractOperation) 
type).getDependencies());
+                }
+            }
+            for (final String name : unnecessary) {
+                final PropertyTypeBuilder property = builder.getProperty(name);
+                // A `false` value below would be a bug, but check anyway.
+                if (property != null && filtered.remove(name) != null) {
+                    property.remove();
+                }
+            }
+        }
+        /*
+         * Remove any remaining operations. The remaining entries shall be 
only expressions
+         * for fetching the values to store in attributes, not values computed 
on-the-fly.
+         */
+        filtered.keySet().removeIf((name) -> {
+            final PropertyTypeBuilder property = builder.getProperty(name);
+            return (property != null) && (property.build() instanceof 
Operation);
+        });
+        /*
+         * Create the new feature types with the modified expressions.
+         * Check if we can reuse the existing feature types.
+         */
+        @SuppressWarnings({"unchecked", "rawtypes"})
+        final Expression<? super Feature, ?>[] filteredExpressions = 
filtered.values().toArray(Expression[]::new);
+        String[] filteredProperties = filtered.keySet().toArray(String[]::new);
+        if (Arrays.equals(filteredProperties, propertiesToCopy)) {
+            filteredProperties = propertiesToCopy;  // Share existing 
instances (common case).
+        }
+        FeatureType withDeps = builder.build();
+        boolean modified = 
builder.setName(typeRequested.getName()).properties().removeIf(
+                (property) -> 
!typeRequested.hasProperty(property.getName().toString()));
+        FeatureType filteredType = builder.build();
+        if (filteredType.equals(typeRequested)) {
+            filteredType = typeRequested;
+        }
+        if (!modified) {
+            withDeps = filteredType;
+        } else if (withDeps.equals(typeWithDependencies)) {
+            withDeps = typeWithDependencies;
         }
-        // TODO: check if we can remove some dependencies.
-        typeWithDependencies = source.typeWithDependencies;
+        var p = new FeatureProjection(filteredType, withDeps, 
filteredProperties, filteredExpressions, createInstance);
+        if (equals(p)) p = this;
+        return p;
     }
 
     /**
@@ -266,9 +380,9 @@ public final class FeatureProjection implements 
UnaryOperator<Feature> {
      * dependencies are references to {@link #typeWithDependencies} properties 
instead of properties of the
      * source features. The property names may differ.</p>
      *
-     * @return all dependencies (including transitive dependencies) as XPaths.
+     * @return all dependencies (including transitive dependencies) from 
source features, as XPaths.
      */
-    public Set<String> dependencies() {
+    public Set<String> requiredSourceProperties() {
         Set<String> references = null;
         for (var expression : expressions) {
             references = ListingPropertyVisitor.xpaths(expression, references);
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
index 282f42986c..d2d84de843 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.feature.internal.shared;
 
+import java.util.Arrays;
 import java.util.Set;
 import java.util.HashSet;
 import org.apache.sis.util.iso.Names;
@@ -128,7 +129,7 @@ public final class FeatureProjectionTest extends TestCase {
         final FeatureProjection projection = builder.project().orElseThrow();
         assertSame(projection.typeRequested, projection.typeWithDependencies);
         assertPropertyNamesEqual(projection.typeRequested, "country", 
"population");
-        assertSetEquals(Set.of("name", "population"), 
projection.dependencies());
+        assertSetEquals(Arrays.asList("name", "population"), 
projection.requiredSourceProperties());
         assertValueClassEquals(String.class, projection, "country");
         return projection;
     }
@@ -205,6 +206,9 @@ public final class FeatureProjectionTest extends TestCase {
     /**
      * Tests the creation of a feature with an additional property computed 
early.
      * The operation is computed immediately from the source feature type.
+     *
+     * Note that since the results are stored (i.e. the "density" operation 
become an attribute in the
+     * final feature instance), there is no need for intermediate feature type 
with extra dependencies.
      */
     @Test
     public void testSubsetWithStoredOperation() {
@@ -214,12 +218,14 @@ public final class FeatureProjectionTest extends TestCase 
{
         addPopulationDensity(builder, true);
         builder.setName("Population density");
         final FeatureProjection projection = builder.project().orElseThrow();
-        assertSame(projection.typeRequested, projection.typeWithDependencies);
-        assertSetEquals(Set.of("name", "population", "area"), 
projection.dependencies());
         assertPropertyNamesEqual(projection.typeRequested, "country", 
"population", "density");
+        assertSetEquals(Arrays.asList("name", "population", "area"), 
projection.requiredSourceProperties());
+
+        // `typeWithDependencies` does not have the "area" extra dependency 
because values are stored.
+        assertSame(projection.typeRequested, projection.typeWithDependencies);
 
         // Property is an attribute because we requested the "stored" mode.
-        assertInstanceOf(AttributeType.class, 
projection.typeWithDependencies.getProperty("density"));
+        assertInstanceOf(AttributeType.class, 
projection.typeRequested.getProperty("density"));
         assertValueClassEquals(Number.class,  projection, "density");
         verifyDensityOnFeatureInstance(projection);
     }
@@ -236,10 +242,10 @@ public final class FeatureProjectionTest extends TestCase 
{
         addPopulationDensity(builder, false);
         builder.setName("Population density");
         final FeatureProjection projection = builder.project().orElseThrow();
-        assertNotEquals(projection.typeRequested, 
projection.typeWithDependencies);
-        assertSetEquals(Set.of("name", "population", "area"), 
projection.dependencies());
+        assertSetEquals(Arrays.asList("name", "population", "area"), 
projection.requiredSourceProperties());
         assertPropertyNamesEqual(projection.typeRequested, "country", 
"population", "density");
         assertPropertyNamesEqual(projection.typeWithDependencies, "country", 
"population", "density", "area");
+        // `typeWithDependencies` needs the extra "area" dependency because 
values are computed.
 
         // Property is an operation because we requested the "deferred" mode.
         assertInstanceOf(Operation.class, 
projection.typeWithDependencies.getProperty("density"));
@@ -251,31 +257,74 @@ public final class FeatureProjectionTest extends TestCase 
{
 
     /**
      * Tests the creation of a feature where an operation has been replaced by 
a simpler one.
-     * This case happen when a pure-Java operation has been replaced by a 
<abbr>SQL</abbr> expression,
+     * This case happens when a pure-Java operation has been replaced by a 
<abbr>SQL</abbr> expression.
+     * The <abbr>SQL</abbr> expression is simulated by a literal, which 
removes the dependency to the
+     * "area" property when an instance of {@link 
FeatureProjection#typeRequested} is created from a
+     * source feature instance.
+     */
+    @Test
+    public void testSubsetWithReplacedOperation() {
+        final FeatureProjection projection = 
testSubsetWithReplacedOperation(true,
+                new HashSet<>(Arrays.asList("country", "population", 
"density")));
+
+        // Property is an operation because we requested the "stored" mode.
+        assertInstanceOf(AttributeType.class, 
projection.typeWithDependencies.getProperty("density"));
+
+        // Compared to `testSubsetWithStoredOperation`, the "area" property 
disaspear.
+        assertSetEquals(Arrays.asList("name", "population"), 
projection.requiredSourceProperties());
+
+        // No extra dependency.
+        assertSame(projection.typeRequested, projection.typeWithDependencies);
+    }
+
+
+    /**
+     * Tests the creation of a feature where an operation has been replaced by 
a simpler one.
+     * This case happens when a pure-Java operation has been replaced by a 
<abbr>SQL</abbr> expression,
      * in which case the expression is simpler from <abbr>SIS</abbr> 
perspective. A consequence of this
      * simplification is that it may remove the need for some dependencies.
      */
     @Test
-    public void testSubsetWithReplacedOperation() {
+    public void testSubsetWithReplacedOperationAndLessDependencies() {
+        final FeatureProjection projection = 
testSubsetWithReplacedOperation(false,
+                new HashSet<>(Arrays.asList("country", "population", 
"density", "area")));
+
+        // Property is an operation because we requested the "deferred" mode.
+        assertInstanceOf(Operation.class, 
projection.typeWithDependencies.getProperty("density"));
+
+        // Compared to `testSubsetWithDeferredOperation`, the "area" property 
disaspear.
+        assertSetEquals(Arrays.asList("name", "population"), 
projection.requiredSourceProperties());
+
+        // The extra "area" dependency should have been removed, resulting in 
the same type.
+        assertSame(projection.typeRequested, projection.typeWithDependencies);
+    }
+
+    /**
+     * Implementation of the {@code testSubsetWithReplacedOperation()} test 
cases.
+     * The "density" operation will be resolved as an attribute if {@code 
stored} is {@code true}.
+     *
+     * @param  stored      whether the "density" operation should be converted 
to an attribute.
+     * @param  attributes  names of the attributes that are expected to found 
in the feature.
+     * @return the projection with modified expressions.
+     */
+    private FeatureProjection testSubsetWithReplacedOperation(final boolean 
stored, final Set<String> attributes) {
         final var builder = new FeatureProjectionBuilder(source, null);
         addCountry(builder);
         addPopulation(builder, false);
-        addPopulationDensity(builder, true);
+        addPopulationDensity(builder, stored);
         builder.setName("Population density");
         FeatureProjection projection = builder.project().orElseThrow();
-        final Set<String> expected = new HashSet<>(Set.of("country", 
"population", "density"));
-        projection = new FeatureProjection(projection, (name, expression) -> {
-            assertTrue(expected.remove(name), name);
+        projection = projection.replaceExpressions((name, expression) -> {
+            assertTrue(attributes.remove(name), name);
             if (name.equals("density")) {
                 return ff.literal(4.08);
             }
             return expression;
         });
-        assertTrue(expected.isEmpty(), expected.toString());
-        assertSame(projection.typeRequested, projection.typeWithDependencies); 
 // Because no more extra dependency.
-        assertSetEquals(Set.of("name", "population"), 
projection.dependencies());
         assertPropertyNamesEqual(projection.typeRequested, "country", 
"population", "density");
+        assertTrue(attributes.isEmpty(), attributes.toString());
         verifyDensityOnFeatureInstance(projection);
+        return projection;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
index fefc2b0473..b475083191 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
@@ -61,7 +61,7 @@ public class Column implements Cloneable {
     public final String name;
 
     /**
-     * Name of the column as declared in with a {@code AS} clause in the 
<abbr>SQL</abbr> statement.
+     * Name of the column as declared with a {@code AS} clause in the 
<abbr>SQL</abbr> statement.
      * This is never null but may be identical to {@link #name} if no {@code 
AS} clause was specified.
      *
      * @see ResultSetMetaData#getColumnLabel(int)
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 e2c49b3962..bdc5cb16a6 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
@@ -461,7 +461,7 @@ final class FeatureStream extends DeferredStream<Feature> {
                 final var columnSQL = new SelectionClause(projected);
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final SelectionClauseWriter filterToSQL = getFilterToSQL();
-                queriedProjection = new FeatureProjection(queriedProjection, 
(name, expression) -> {
+                queriedProjection = 
queriedProjection.replaceExpressions((name, expression) -> {
                     final JDBCType type = filterToSQL.writeFunction(columnSQL, 
expression);
                     if (type != null) try {
                         columnSQL.append(" AS ").appendIdentifier(name);
@@ -473,9 +473,6 @@ final class FeatureStream extends DeferredStream<Feature> {
                     columnSQL.clear();
                     return expression;
                 });
-                if (queriedProjection.equals(projection)) {
-                    queriedProjection = projection;     // No change, keep the 
original one.
-                }
             }
             /*
              * Build a pseudo-table (a view) with the subset of columns 
specified by the projection.
@@ -484,7 +481,7 @@ final class FeatureStream extends DeferredStream<Feature> {
             final var reusedNames = new HashSet<String>();
             projected = new Table(projected, queriedProjection, reusedNames, 
unhandled);
             completion = 
queriedProjection.forPreexistingFeatureInstances(unhandled.stream().toArray());
-            if (completion != null && 
!reusedNames.containsAll(completion.dependencies())) {
+            if (completion != null && 
!reusedNames.containsAll(completion.requiredSourceProperties())) {
                 /*
                  * Cannot use `projected` because some expressions need 
properties available only
                  * in the source features. Request full feature instances from 
the original table.


Reply via email to