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 d8bbaf214a Clarify which `Feature` instance is used for the different 
projection types. Other opportunistic clarifications as we tried to verify the 
behavior. Replace `ExpressionOperation` by `LinkOperation` when applicable.
d8bbaf214a is described below

commit d8bbaf214a1c60b20a7928bd11457b13b7061594
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue May 2 19:29:16 2023 +0200

    Clarify which `Feature` instance is used for the different projection types.
    Other opportunistic clarifications as we tried to verify the behavior.
    Replace `ExpressionOperation` by `LinkOperation` when applicable.
---
 .../org/apache/sis/feature/AbstractFeature.java    | 12 ++-
 .../java/org/apache/sis/feature/DenseFeature.java  |  1 +
 .../org/apache/sis/feature/EnvelopeOperation.java  | 21 +----
 .../apache/sis/feature/ExpressionOperation.java    | 65 +++++++++++++---
 .../org/apache/sis/feature/FeatureOperations.java  | 25 +++---
 .../org/apache/sis/feature/OperationResult.java    | 71 +++++++++++++++++
 .../java/org/apache/sis/feature/SparseFeature.java |  1 +
 .../apache/sis/feature/StringJoinOperation.java    | 13 +---
 .../java/org/apache/sis/filter/PropertyValue.java  |  1 +
 .../apache/sis/{ => internal}/filter/XPath.java    |  8 +-
 .../test/java/org/apache/sis/filter/XPathTest.java |  1 +
 .../org/apache/sis/internal/map/SEPortrayer.java   |  5 +-
 .../java/org/apache/sis/storage/FeatureQuery.java  | 90 +++++++++++++++++-----
 .../java/org/apache/sis/storage/FeatureSubset.java |  2 +-
 .../org/apache/sis/storage/FeatureQueryTest.java   |  8 +-
 15 files changed, 246 insertions(+), 78 deletions(-)

diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java 
b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java
index 3d823e76e7..becfe57dfb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java
@@ -67,6 +67,13 @@ import org.opengis.feature.Operation;
  *   <li>the cardinality of all attributes is constrained to [1 … 1].</li>
  * </ul>
  *
+ * <h2>Operations</h2>
+ * Properties that are instances of {@link Operation} are usually not stored 
in {@code Feature} instances.
+ * Instead, the {@link Operation#apply Operation.apply(…)} method is invoked 
every times that the property
+ * value is requested. {@code AbstractFeature} does not cache operation 
results.
+ * Those results are usually read-only, but may be writable under the 
conditions documented in
+ * {@link #setOperationValue(String, Object)}.
+ *
  * <h2>Limitations</h2>
  * <ul>
  *   <li><b>Multi-threading:</b> {@code AbstractFeature} instances are 
<strong>not</strong> thread-safe.
@@ -439,7 +446,10 @@ public abstract class AbstractFeature implements Feature, 
Serializable {
      * Executes the parameterless operation of the given name and sets the 
value of its result.
      * This method is the complement of {@link #getOperationValue(String)} for 
subclasses where
      * some properties may be operations. Not all operations accept 
assignments,
-     * but the {@linkplain FeatureOperations#link link} operation for instance 
does.
+     * but the {@linkplain FeatureOperations#link link} and
+     * {@linkplain FeatureOperations#compound compound} operations (for 
instances) do.
+     * Whether an operation is writable or not depends on whether the computed 
{@link Property}
+     * supports {@link Attribute#setValue(Object)} or {@link 
FeatureAssociation#setValue(Feature)}.
      *
      * @param  name   the name of the operation to execute. The caller is 
responsible to ensure that the
      *                property type for that name is an instance of {@link 
Operation}.
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java 
b/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java
index 319a81f9d1..54193162b0 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java
@@ -60,6 +60,7 @@ final class DenseFeature extends AbstractFeature implements 
Cloneable {
 
     /**
      * The properties (attributes or feature associations) in this feature.
+     * This array does not include operation results, which are always 
computed on the fly.
      *
      * Conceptually, values in this array are {@link Property} instances. 
However, at first we will store only
      * the property <em>values</em>, and convert to an array of type {@code 
Property[]} only when at least one
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java 
b/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java
index 6018a951d6..88594a2fa2 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java
@@ -272,24 +272,17 @@ final class EnvelopeOperation extends AbstractOperation {
      * The attributes that contains the result of union of all envelope 
extracted from other attributes.
      * Value is calculated each time it is accessed.
      */
-    private final class Result extends AbstractAttribute<Envelope> {
+    private final class Result extends OperationResult<Envelope> {
         /**
          * For cross-version compatibility.
          */
-        private static final long serialVersionUID = 926172863066901618L;
-
-        /**
-         * The feature specified to the {@link 
StringJoinOperation#apply(Feature, ParameterValueGroup)} method.
-         */
-        @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
-        private final Feature feature;
+        private static final long serialVersionUID = 4900962888075807964L;
 
         /**
          * Creates a new attribute for the given feature.
          */
         Result(final Feature feature) {
-            super(resultType);
-            this.feature = feature;
+            super(resultType, feature);
         }
 
         /**
@@ -406,14 +399,6 @@ final class EnvelopeOperation extends AbstractOperation {
                 throw new 
FeatureOperationException(Errors.formatInternational(Errors.Keys.CanNotTransformEnvelope),
 e);
             }
         }
-
-        /**
-         * Unconditionally throws an {@link UnsupportedOperationException}.
-         */
-        @Override
-        public void setValue(Envelope value) {
-            throw new 
UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, 
Attribute.class));
-        }
     }
 
     /**
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
 
b/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
index c5990072e8..7b897b979a 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
@@ -31,7 +31,6 @@ import org.apache.sis.internal.filter.Visitor;
 // Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.Property;
-import org.opengis.feature.Attribute;
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.IdentifiedType;
 import org.opengis.filter.Filter;
@@ -70,7 +69,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<? super V> result;
+    private final AttributeType<V> resultType;
 
     /**
      * The name of all feature properties that are known to be read by the 
expression.
@@ -85,17 +84,35 @@ final class ExpressionOperation<V> extends 
AbstractOperation {
      *
      * @param identification  the name of the operation, together with 
optional information.
      * @param expression      the expression to evaluate on feature instances.
-     * @param result          type of values computed by the expression.
+     * @param resultType      type of values computed by the expression.
      */
-    ExpressionOperation(final Map<String,?> identification,
-                        final Function<? super Feature, ? extends V> 
expression,
-                        final AttributeType<? super V> result)
+    static <V> AbstractOperation create(final Map<String,?> identification,
+                                        final Function<? super Feature, ? 
extends V> expression,
+                                        final AttributeType<? super V> 
resultType)
+    {
+        if (expression instanceof ValueReference<?,?>) {
+            final String xpath = ((ValueReference<?,?>) expression).getXPath();
+            if (xpath.equals(resultType.getName().toString())) {
+                return new LinkOperation(identification, resultType);
+            }
+        }
+        return new ExpressionOperation<>(identification, expression, 
resultType);
+    }
+
+    /**
+     * Creates a generic operation when no optimized case has been identifier.
+     */
+    private ExpressionOperation(final Map<String,?> identification,
+                                final Function<? super Feature, ? extends V> 
expression,
+                                final AttributeType<V> resultType)
     {
         super(identification);
         this.expression = expression;
-        this.result     = result;
+        this.resultType = resultType;
         if (expression instanceof Expression<?,?>) {
-            dependencies = DependencyFinder.search((Expression<Feature,?>) 
expression);
+            @SuppressWarnings("unchecked")
+            var c = (Expression<Feature,?>) expression;     // Cast is okay 
because we will not pass or request any `Feature` instance.
+            dependencies = DependencyFinder.search(c);
         } else {
             dependencies = Set.of();
         }
@@ -114,7 +131,7 @@ final class ExpressionOperation<V> extends 
AbstractOperation {
      */
     @Override
     public IdentifiedType getResult() {
-        return result;
+        return resultType;
     }
 
     /**
@@ -136,9 +153,33 @@ final class ExpressionOperation<V> extends 
AbstractOperation {
      */
     @Override
     public Property apply(final Feature feature, ParameterValueGroup 
parameters) {
-        final Attribute<? super V> instance = result.newInstance();
-        instance.setValue(expression.apply(feature));
-        return instance;
+        return new Result(feature);
+    }
+
+    /**
+     * The attributes that delegates computation to the expression.
+     * Value is calculated each time it is accessed.
+     */
+    private final class Result extends OperationResult<V> {
+        /**
+         * For cross-version compatibility.
+         */
+        private static final long serialVersionUID = -19004252522001532L;
+
+        /**
+         * Creates a new attribute for the given feature.
+         */
+        Result(final Feature feature) {
+            super(resultType, feature);
+        }
+
+        /**
+         * Delegates the computation to the user-supplied expression.
+         */
+        @Override
+        public V getValue() {
+            return expression.apply(feature);
+        }
     }
 
     /**
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java 
b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
index 15c36b01cb..db15ddcb83 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
@@ -130,7 +130,7 @@ public final class FeatureOperations extends Static {
      * Creates an operation which is only an alias for another property.
      *
      * <h4>Example</h4>
-     * features often have a property that can be used as identifier or 
primary key.
+     * Features often have a property that can be used as identifier or 
primary key.
      * But the name of that property may vary between features of different 
types.
      * For example, features of type <b>Country</b> may have identifiers named 
“ISO country code”
      * while features of type <b>Car</b> may have identifiers named “license 
plate number”.
@@ -274,22 +274,23 @@ public final class FeatureOperations extends Static {
      * @param  <V>             the type of values computed by the expression 
and assigned to the feature property.
      * @param  identification  the name of the operation, together with 
optional information.
      * @param  expression      the expression to evaluate on feature instances.
-     * @param  result          type of values computed by the expression and 
assigned to the feature property.
+     * @param  resultType      type of values computed by the expression and 
assigned to the feature property.
      * @return a feature operation which computes its values using the given 
expression.
      *
      * @since 1.4
      */
-    public static <V> Operation expression(final Map<String,?> identification,
-                                           final Function<? super Feature, ? 
extends V> expression,
-                                           final AttributeType<? super V> 
result)
+    public static <V> Operation function(final Map<String,?> identification,
+                                         final Function<? super Feature, ? 
extends V> expression,
+                                         final AttributeType<? super V> 
resultType)
     {
         ArgumentChecks.ensureNonNull("expression", expression);
-        return POOL.unique(new ExpressionOperation<>(identification, 
expression, result));
+        ArgumentChecks.ensureNonNull("result", resultType);
+        return POOL.unique(ExpressionOperation.create(identification, 
expression, resultType));
     }
 
     /**
      * Creates an operation which delegates the computation to a given 
expression producing values of unknown type.
-     * This method can be used as an alternative to {@link #expression 
expression(…)} when the constraint on the
+     * This method can be used as an alternative to {@link #function 
function(…)} when the constraint on the
      * parameterized type {@code <V>} between {@code expression} and {@code 
result} cannot be enforced at compile time.
      * This method casts or converts the expression to the expected type by a 
call to
      * {@link Expression#toValueType(Class)}.
@@ -297,16 +298,16 @@ public final class FeatureOperations extends Static {
      * @param  <V>             the type of values computed by the expression 
and assigned to the feature property.
      * @param  identification  the name of the operation, together with 
optional information.
      * @param  expression      the expression to evaluate on feature instances.
-     * @param  result          type of values computed by the expression and 
assigned to the feature property.
+     * @param  resultType      type of values computed by the expression and 
assigned to the feature property.
      * @return a feature operation which computes its values using the given 
expression.
      * @throws ClassCastException if the result type is not a target type 
supported by the expression.
      *
      * @since 1.4
      */
-    public static <V> Operation expressionToResult(final Map<String,?> 
identification,
-                                                   final Expression<? super 
Feature, ?> expression,
-                                                   final AttributeType<V> 
result)
+    public static <V> Operation expression(final Map<String,?> identification,
+                                           final Expression<? super Feature, 
?> expression,
+                                           final AttributeType<V> resultType)
     {
-        return expression(identification, 
expression.toValueType(result.getValueClass()), result);
+        return function(identification, 
expression.toValueType(resultType.getValueClass()), resultType);
     }
 }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/OperationResult.java 
b/core/sis-feature/src/main/java/org/apache/sis/feature/OperationResult.java
new file mode 100644
index 0000000000..b05f884c54
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/OperationResult.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.feature;
+
+import org.apache.sis.util.resources.Errors;
+
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+import org.opengis.feature.Attribute;
+import org.opengis.feature.AttributeType;
+
+
+/**
+ * Base class of attributes that are the result of a feature operation.
+ * This base class is defined for making easier to identify where computations 
are done.
+ *
+ * @todo A future version may provide caching services, methods for taking a 
snapshot, <i>etc.</i>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ *
+ * @param <V> the type of attribute values.
+ *
+ * @since 1.4
+ */
+abstract class OperationResult<V> extends AbstractAttribute<V> {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 1418917854672134381L;
+
+    /**
+     * The feature instance to use as a source for computing the result.
+     */
+    @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
+    protected final Feature feature;
+
+    /**
+     * Creates a new operation for a result of the given type.
+     *
+     * @param type  information about the attribute (base Java class, domain 
of values, <i>etc.</i>).
+     */
+    protected OperationResult(final AttributeType<V> type, final Feature 
feature) {
+        super(type);
+        this.feature = feature;
+    }
+
+    /**
+     * Retro-propagate an operation result to the properties in the source 
feature instance.
+     * This is an optional operation.
+     * The default implementation unconditionally throws an {@link 
UnsupportedOperationException}.
+     */
+    @Override
+    public void setValue(V value) {
+        throw new 
UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, 
Attribute.class));
+    }
+}
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java 
b/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java
index 55af5252ed..7b3ddce242 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java
@@ -82,6 +82,7 @@ final class SparseFeature extends AbstractFeature implements 
Cloneable {
 
     /**
      * The properties (attributes or feature associations) in this feature.
+     * This map does not include operation results, which are always computed 
on the fly.
      *
      * Conceptually, values in this map are {@link Property} instances. 
However, at first we will store
      * only the property <em>values</em>, and build the full {@code Property} 
objects only if they are
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
 
b/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
index a618de04a4..3bc72ab684 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java
@@ -317,24 +317,17 @@ final class StringJoinOperation extends AbstractOperation 
{
      * The attributes that contains the result of concatenating the string 
representation of other attributes.
      * Value is calculated each time it is accessed.
      */
-    private final class Result extends AbstractAttribute<String> {
+    private final class Result extends OperationResult<String> {
         /**
          * For cross-version compatibility.
          */
-        private static final long serialVersionUID = -8435975199763452547L;
-
-        /**
-         * The feature specified to the {@link 
StringJoinOperation#apply(Feature, ParameterValueGroup)} method.
-         */
-        @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
-        private final Feature feature;
+        private static final long serialVersionUID = -555025854115540108L;
 
         /**
          * Creates a new attribute for the given feature.
          */
         Result(final Feature feature) {
-            super(resultType);
-            this.feature = feature;
+            super(resultType, feature);
         }
 
         /**
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java 
b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
index cae63556d3..4fb0bcbe1a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
@@ -26,6 +26,7 @@ import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.feature.builder.PropertyTypeBuilder;
 import org.apache.sis.feature.builder.AttributeTypeBuilder;
+import org.apache.sis.internal.filter.XPath;
 import org.apache.sis.util.resources.Errors;
 
 // Branch-dependent imports
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/XPath.java 
b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/XPath.java
similarity index 90%
rename from core/sis-feature/src/main/java/org/apache/sis/filter/XPath.java
rename to 
core/sis-feature/src/main/java/org/apache/sis/internal/filter/XPath.java
index c1ca1cccb6..aa40ecceba 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/XPath.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/XPath.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.filter;
+package org.apache.sis.internal.filter;
 
 import java.util.List;
 import java.util.ArrayList;
@@ -32,9 +32,11 @@ import static org.apache.sis.util.CharSequences.*;
  * @version 1.3
  * @since   0.4
  */
-final class XPath extends Static {
+public final class XPath extends Static {
     /**
      * The separator between path components.
+     * Should not be used for URL or Unix name separator, even if the 
character is the same.
+     * We use this constant for identifying locations in the code where there 
is some X-Path parsing.
      */
     public static final char SEPARATOR = '/';
 
@@ -57,7 +59,7 @@ final class XPath extends Static {
      *         if there is no separator. If non-null, the list always contains 
at least one element.
      * @throws IllegalArgumentException if the XPath contains at least one 
empty component.
      */
-    static List<String> split(final String xpath) {
+    public static List<String> split(final String xpath) {
         int next = xpath.indexOf(SEPARATOR);
         if (next < 0) {
             return null;
diff --git 
a/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java 
b/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java
index 2d7e4e599c..a8f11aa454 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.filter;
 
+import org.apache.sis.internal.filter.XPath;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
index 61da8fd80e..6eff0d15a4 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java
@@ -42,6 +42,7 @@ import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.internal.filter.XPath;
 import org.apache.sis.storage.FeatureQuery;
 import org.apache.sis.portrayal.MapItem;
 import org.apache.sis.portrayal.MapLayer;
@@ -710,7 +711,7 @@ public final class SEPortrayer {
      * Removes any xpath elements, keep only the root property name.
      */
     private static String stripXpath(String attName) {
-        int index = attName.indexOf('/');
+        int index = attName.indexOf(XPath.SEPARATOR);
         if (index == 0) {
             attName = attName.substring(1);             // Remove first slash
             final Pattern pattern = 
Pattern.compile("(\\{[^\\{\\}]*\\})|(\\[[^\\[\\]]*\\])|/{1}");
@@ -722,7 +723,7 @@ matches:    while (matcher.find()) {
                 sb.append(attName.substring(position, matcher.start()));
                 position = matcher.end();
                 switch (match.charAt(0)) {
-                    case '/': {
+                    case XPath.SEPARATOR: {
                         // We do not query precisely sub elements.
                         position = attName.length();
                         break matches;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
index 0f4bb9e3e7..46947ce71e 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
@@ -37,10 +37,12 @@ import org.apache.sis.feature.builder.AttributeTypeBuilder;
 import org.apache.sis.internal.feature.AttributeConvention;
 import org.apache.sis.internal.feature.FeatureExpression;
 import org.apache.sis.internal.filter.SortByComparator;
+import org.apache.sis.internal.filter.XPath;
 import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.filter.Optimization;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.iso.Names;
@@ -254,6 +256,25 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
         return (projection != null) ? projection.clone() : null;
     }
 
+    /**
+     * Returns the properties to be stored in the target feature.
+     */
+    final NamedExpression[] getStoredProjection() {
+        final NamedExpression[] stored = getProjection();
+        if (stored != null) {
+            int count = 0;
+            for (final NamedExpression p : stored) {
+                if (p.type == ProjectionType.STORED) {
+                    stored[count++] = p;
+                }
+            }
+            if (count != 0) {
+                return ArraysExt.resize(stored, count);
+            }
+        }
+        return null;
+    }
+
     /**
      * Sets the approximate area of feature instances to include in the subset.
      * This convenience method creates a filter that checks if the bounding box
@@ -430,9 +451,33 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
          * and the result is stored as a feature attribute.
          * The feature property type will be {@link Attribute} and its value 
will be modifiable.
          * This is the default projection type.
+         *
+         * <h4>Feature instances in expression evaluation</h4>
+         * The features given in calls to {@link Expression#apply(Object)} are 
instances from the
+         * <em>source</em> {@link FeatureSet}, before filtering.
          */
         STORED,
 
+        /*
+         * The expression is evaluated every times that the property value is 
requested.
+         * This projection type is similar to {@link #COMPLETING}, except that 
the features
+         * given in calls to {@link Expression#apply(Object)} are the same 
instances than
+         * the ones used by {@link #STORED}.
+         *
+         * <div class="note"><b>Note on naming:</b>
+         * the {@code STORED} and {@code VIRTUAL} enumeration values are named 
according usage in SQL
+         * {@code GENERATE ALWAYS} statement. Those two keywords work on 
columns in the source tables.
+         * </div>
+         *
+         * <h4>Feature instances in expression evaluation</h4>
+         * The combination of deferred calculation (like {@link #COMPLETING}) 
and usage of feature instances
+         * from the <em>source</em> {@link FeatureSet} (like {@link #STORED}) 
may cause this projection type
+         * to retain the source feature instances for a longer time than other 
types.
+         *
+         * @todo Waiting to see if there is a need for this type before to 
implement it.
+         */
+      // VIRTUAL,
+
         /**
          * The expression is evaluated every times that the property value is 
requested.
          * The feature property type will be {@link Operation}.
@@ -445,14 +490,19 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
          *   <li>Computation result should not be stored in order to reduce 
memory usage.</li>
          * </ul>
          *
+         * <h4>Feature instances in expression evaluation</h4>
+         * The features given in calls to {@link Expression#apply(Object)} are 
instances from the <em>target</em>
+         * {@link FeatureSet}, after filtering. The instances from the source 
{@code FeatureSet} are no longer
+         * available when the expression is executed. Consequently, all fields 
that are necessary for computing
+         * a {@code COMPLETING} field shall have been first copied in {@link 
#STORED} fields.
+         *
+         * <div class="note"><b>Note on naming:</b>
+         * verb tense <i>-ing</i> instead of <i>-ed</i> is for emphasizing 
that the data used for computation
+         * are current (filtered) data instead of past (original) data.</div>
+         *
          * @see FeatureOperations#expression(Map, Function, AttributeType)
          */
-        VIRTUAL
-
-        /*
-         * Examples of other enumeration values that we may add in the future:
-         * GENERATED for meaning "STORED but read-only", CACHED for lazy 
computation.
-         */
+        COMPLETING
     }
 
     /**
@@ -586,10 +636,9 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
          */
         @Override
         public String toString() {
-            final StringBuilder buffer = new StringBuilder();
-            buffer.append(getClass().getSimpleName()).append('[');      // 
Class name without enclosing class.
+            final StringBuilder buffer = new StringBuilder("SELECT ");
             appendTo(buffer);
-            return buffer.append(']').toString();
+            return buffer.toString();
         }
 
         /**
@@ -597,11 +646,14 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
          */
         final void appendTo(final StringBuilder buffer) {
             if (expression instanceof Literal<?,?>) {
-                buffer.append('“').append(((Literal<?,?>) 
expression).getValue()).append('”');
+                buffer.append('‘').append(((Literal<?,?>) 
expression).getValue()).append('’');
             } else if (expression instanceof ValueReference<?,?>) {
-                buffer.append(((ValueReference<?,?>) expression).getXPath());
+                buffer.append('“').append(((ValueReference<?,?>) 
expression).getXPath()).append('”');
             } else {
-                buffer.append(expression.getFunctionName());
+                
buffer.append("=“").append(expression.getFunctionName()).append("”()");
+            }
+            if (type != ProjectionType.STORED) {
+                buffer.append(' ').append(type);
             }
             if (alias != null) {
                 buffer.append(" AS “").append(alias).append('”');
@@ -721,7 +773,7 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
                         continue;
                     }
                     String xpath = ((ValueReference<?,?>) 
expression).getXPath().trim();
-                    xpath = xpath.substring(xpath.lastIndexOf('/') + 1);    // 
Works also if '/' is not found.
+                    xpath = xpath.substring(xpath.lastIndexOf(XPath.SEPARATOR) 
+ 1);  // Works also if '/' is not found.
                     if (!(xpath.isEmpty() || names.contains(xpath))) {
                         text = xpath;
                     }
@@ -737,18 +789,20 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
                 } while (!names.add(text.toString()));
                 name = Names.createLocalName(null, null, text);
             }
-            resultType.setName(name);
             /*
-             * If the attribute that we just added should be virtual,
-             * replace the attribute by an operation.
+             * If the attribute that we just added should be virtual, replace 
the attribute by an operation.
+             * We need to keep the property name computed by 
`fex.expectedType(…)` for the operation result,
+             * because that name is the name of the link to create if the 
operation is `ValueReference`.
              */
-            if (item.type == ProjectionType.VIRTUAL && resultType instanceof 
AttributeTypeBuilder<?>) {
+            if (item.type == ProjectionType.COMPLETING && resultType 
instanceof AttributeTypeBuilder<?>) {
                 final var ab = (AttributeTypeBuilder<?>) resultType;
                 final AttributeType<?> storedType = ab.build();
                 if (ftb.properties().remove(resultType)) {
                     final var properties = Map.of(AbstractOperation.NAME_KEY, 
name);
-                    
ftb.addProperty(FeatureOperations.expressionToResult(properties, expression, 
storedType));
+                    ftb.addProperty(FeatureOperations.expression(properties, 
expression, storedType));
                 }
+            } else {
+                resultType.setName(name);
             }
         }
         return ftb.build();
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java
index dc4a9f6992..5b2e67c900 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java
@@ -139,7 +139,7 @@ final class FeatureSubset extends AbstractFeatureSet {
          * Transform feature instances.
          * Note: "projection" here is in relational database sense, not map 
projection.
          */
-        final FeatureQuery.NamedExpression[] projection = 
query.getProjection();
+        final FeatureQuery.NamedExpression[] projection = 
query.getStoredProjection();
         if (projection != null) {
             @SuppressWarnings({"unchecked", "rawtypes"})
             final Expression<? super Feature,?>[] expressions = new 
Expression[projection.length];
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
index b4aa6f3786..fb70453eb6 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.stream.Collectors;
+import org.apache.sis.feature.Features;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.internal.storage.MemoryFeatureSet;
 import org.apache.sis.filter.DefaultFilterFactory;
@@ -328,7 +329,7 @@ public final class FeatureQueryTest extends TestCase {
      * Shortcut for creating expression for a projection computed on-the-fly.
      */
     private static FeatureQuery.NamedExpression virtualProjection(final 
Expression<Feature, ?> expression, final String alias) {
-        return new FeatureQuery.NamedExpression(expression, 
Names.createLocalName(null, null, alias), FeatureQuery.ProjectionType.VIRTUAL);
+        return new FeatureQuery.NamedExpression(expression, 
Names.createLocalName(null, null, alias), 
FeatureQuery.ProjectionType.COMPLETING);
     }
 
     /**
@@ -367,6 +368,11 @@ public final class FeatureQueryTest extends TestCase {
         assertEquals(3, instance.getPropertyValue("value1"));
         assertEquals(3, instance.getPropertyValue("renamed1"));
         assertEquals("a literal", instance.getPropertyValue("computed"));
+
+        // The `ValueReference` operation should have been optimized as a link.
+        assertEquals("value1", Features.getLinkTarget(pt2).get());
+        assertTrue(Features.getLinkTarget(pt1).isEmpty());
+        assertTrue(Features.getLinkTarget(pt3).isEmpty());
     }
 
     /**


Reply via email to