You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/04/28 13:31:29 UTC

[sis] 01/01: Merge remote-tracking branch 'origin/feat/computed-fields' into geoapi-4.0.

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 511ec7b89fc8dc1b94dfd9158282b5281d67e988
Merge: f6509802be 3704683e32
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Apr 28 15:31:07 2023 +0200

    Merge remote-tracking branch 'origin/feat/computed-fields' into geoapi-4.0.

 .../apache/sis/feature/ExpressionOperation.java    | 227 +++++++++++++++++++++
 .../org/apache/sis/feature/FeatureOperations.java  |  51 ++++-
 .../java/org/apache/sis/feature/LinkOperation.java |   2 +-
 .../sis/feature/builder/AttributeTypeBuilder.java  |   1 +
 .../sis/internal/feature/FeatureExpression.java    |  14 ++
 .../java/org/apache/sis/storage/FeatureQuery.java  | 135 ++++++++++--
 .../org/apache/sis/storage/FeatureQueryTest.java   |  63 +++++-
 7 files changed, 473 insertions(+), 20 deletions(-)

diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
index 0000000000,94349dec0c..78ecc4b7ad
mode 000000,100644..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
@@@ -1,0 -1,146 +1,227 @@@
+ /*
+  * 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 java.util.Collection;
 -import java.util.Collections;
 -import java.util.HashSet;
++import java.util.Map;
+ import java.util.Set;
 -import org.apache.sis.feature.builder.FeatureTypeBuilder;
 -import org.apache.sis.feature.builder.PropertyTypeBuilder;
 -import org.apache.sis.internal.feature.FeatureExpression;
++import java.util.HashSet;
++import java.util.Collection;
++import java.util.function.Function;
++import org.opengis.util.CodeList;
++import org.opengis.parameter.ParameterValueGroup;
++import org.opengis.parameter.ParameterDescriptorGroup;
+ import org.apache.sis.internal.feature.FeatureUtilities;
+ import org.apache.sis.internal.filter.FunctionNames;
+ 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.Feature;
 -import org.opengis.feature.FeatureType;
+ import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.Property;
 -import org.opengis.filter.BetweenComparisonOperator;
 -import org.opengis.filter.ComparisonOperatorName;
 -import org.opengis.filter.Expression;
+ import org.opengis.filter.Filter;
 -import org.opengis.filter.LikeOperator;
++import org.opengis.filter.Expression;
+ import org.opengis.filter.LogicalOperator;
+ import org.opengis.filter.ValueReference;
 -import org.opengis.parameter.ParameterDescriptorGroup;
 -import org.opengis.parameter.ParameterValueGroup;
 -import org.opengis.util.CodeList;
 -import org.opengis.util.GenericName;
++
+ 
+ /**
 - * An operation computing the result of expression on current feature.
++ * A feature property which is an operation implemented by a filter expression.
++ * This operation computes expression results from given feature instances only,
++ * there is no parameters.
+  *
 - * @author Johann Sorel (Geomatys)
++ * @author  Johann Sorel (Geomatys)
++ * @version 1.4
++ * @since   1.4
+  */
 -public final class ExpressionOperation<V> extends AbstractOperation {
++final class ExpressionOperation<V> extends AbstractOperation {
++    /**
++     * For cross-version compatibility.
++     */
++    private static final long serialVersionUID = 5411697964136428848L;
++
++    /**
++     * The parameter descriptor for the "Expression" operation, which does not take any parameter.
++     */
++    private static final ParameterDescriptorGroup PARAMETERS = FeatureUtilities.parameters("Expression");
+ 
+     /**
 -     * The parameter descriptor for the "virtual" operation, which does not take any parameter.
++     * The expression on which to delegate the execution of this operation.
+      */
 -    private static final ParameterDescriptorGroup EMPTY_PARAMS = FeatureUtilities.parameters("Virtual");
++    @SuppressWarnings("serial")                         // Not statically typed as serializable.
++    private final Function<? super Feature, ? extends V> expression;
+ 
 -    private static final ListPropertyVisitor VISITOR = new ListPropertyVisitor();
++    /**
++     * The type of result of evaluating the expression.
++     */
++    @SuppressWarnings("serial")                         // Apache SIS implementations are serializable.
++    private final AttributeType<? super V> result;
+ 
 -    private final FeatureExpression<Feature,V> expression;
 -    private final AttributeType<V> type;
++    /**
++     * The name of all feature properties that are known to be read by the expression.
++     * This is determined by execution of {@link #VISITOR} on the {@linkplain #expression}.
++     * This set may be incomplete if some properties are read otherwise than by {@link ValueReference}.
++     */
++    @SuppressWarnings("serial")                         // Set.of(…) implementations are serializable.
+     private final Set<String> dependencies;
+ 
 -    public ExpressionOperation(GenericName name, FeatureExpression<Feature,V> expression, FeatureType featureType) {
 -        super(Collections.singletonMap(DefaultAttributeType.NAME_KEY, name));
++    /**
++     * Creates a new operation which will delegate execution to the given expression.
++     *
++     * @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.
++     */
++    ExpressionOperation(final Map<String,?> identification,
++                        final Function<? super Feature, ? extends V> expression,
++                        final AttributeType<? super V> result)
++    {
++        super(identification);
+         this.expression = expression;
 -        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
 -        PropertyTypeBuilder expectedType = expression.expectedType(featureType, ftb);
 -        expectedType.setName(name);
 -        type = (AttributeType<V>) expectedType.build();
 -
 -        final Set<String> dependencies = new HashSet<>();
 -        VISITOR.visit((Expression) expression, dependencies);
 -        this.dependencies = Collections.unmodifiableSet(dependencies);
 -    }
 -
 -    public FeatureExpression<Feature, V> getExpression() {
 -        return expression;
++        this.result     = result;
++        if (expression instanceof Expression<?,?>) {
++            dependencies = DependencyFinder.search((Expression<Object,?>) expression);
++        } else {
++            dependencies = Set.of();
++        }
+     }
+ 
++    /**
++     * Returns a description of the input parameters.
++     */
+     @Override
+     public ParameterDescriptorGroup getParameters() {
 -        return EMPTY_PARAMS;
++        return PARAMETERS;
+     }
+ 
++    /**
++     * Returns the expected result type.
++     */
+     @Override
+     public IdentifiedType getResult() {
 -        return type;
++        return result;
+     }
+ 
++    /**
++     * Returns the names of feature properties that this operation needs for performing its task.
++     * This set may be incomplete if some properties are read otherwise than by {@link ValueReference}.
++     */
+     @Override
++    @SuppressWarnings("ReturnOfCollectionOrArrayField")     // Because the set is unmodifiable.
+     public Set<String> getDependencies() {
+         return dependencies;
+     }
+ 
++    /**
++     * Returns the value computed by the expression for the given feature instance.
++     *
++     * @param  feature     the feature to evaluate with the expression.
++     * @param  parameters  ignored (can be {@code null}).
++     * @return the computed property from the given feature.
++     */
+     @Override
 -    public Property apply(Feature feature, ParameterValueGroup parameters) {
 -        final Attribute<V> att = type.newInstance();
 -        att.setValue(expression.apply(feature));
 -        return att;
++    public Property apply(final Feature feature, ParameterValueGroup parameters) {
++        final Attribute<? super V> instance = result.newInstance();
++        instance.setValue(expression.apply(feature));
++        return instance;
+     }
+ 
 -    private static final class ListPropertyVisitor extends Visitor<Object,Collection<String>> {
++    /**
++     * Computes a hash-code value for this operation.
++     */
++    @Override
++    public int hashCode() {
++        return super.hashCode() + expression.hashCode();
++    }
++
++    /**
++     * Compares this operation with the given object for equality.
++     */
++    @Override
++    public boolean equals(final Object obj) {
++        /*
++         * `this.result` is compared (indirectly) by the super class.
++         * `this.dependencies` does not need to be compared because it is derived from `expression`.
++         */
++        return super.equals(obj) && expression.equals(((ExpressionOperation) obj).expression);
++    }
+ 
 -        protected ListPropertyVisitor() {
 -            setLogicalHandlers((f, names) -> {
 -                final LogicalOperator<Object> filter = (LogicalOperator<Object>) f;
++    /**
++     * An expression visitor for finding all dependencies of a given expression.
++     * The dependencies are feature properties read by {@link ValueReference} nodes.
++     *
++     * @todo The first parameterized type should be {@code Feature} instead of {@code Object}.
++     */
++    private static final class DependencyFinder extends Visitor<Object, Collection<String>> {
++        /**
++         * The unique instance.
++         */
++        private static final DependencyFinder VISITOR = new DependencyFinder();
++
++        /**
++         * Returns all dependencies read by a {@link ValueReference} node.
++         *
++         * @param  expression  the expression for which to get dependencies.
++         * @return all dependencies recognized by this method.
++         */
++        static Set<String> search(final Expression<Object,?> expression) {
++            final Set<String> dependencies = new HashSet<>();
++            VISITOR.visit(expression, dependencies);
++            return Set.copyOf(dependencies);
++        }
++
++        /**
++         * Constructor for the unique instance.
++         */
++        private DependencyFinder() {
++            setLogicalHandlers((f, dependencies) -> {
++                final var filter = (LogicalOperator<Object>) f;
+                 for (Filter<Object> child : filter.getOperands()) {
 -                    visit(child, names);
++                    visit(child, dependencies);
+                 }
+             });
 -            setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_BETWEEN), (f, names) -> {
 -                final BetweenComparisonOperator<Object> filter = (BetweenComparisonOperator<Object>) f;
 -                visit(filter.getExpression(),    names);
 -                visit(filter.getLowerBoundary(), names);
 -                visit(filter.getUpperBoundary(), names);
 -            });
 -            setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_LIKE), (f, names) -> {
 -                final LikeOperator<Object> filter = (LikeOperator<Object>) f;
 -                visit(filter.getExpressions().get(0), names);
 -            });
 -            setExpressionHandler(FunctionNames.ValueReference, (e, names) -> {
 -                final ValueReference<Object,?> expression = (ValueReference<Object,?>) e;
++            setExpressionHandler(FunctionNames.ValueReference, (e, dependencies) -> {
++                final var expression = (ValueReference<Object,?>) e;
+                 final String propName = expression.getXPath();
+                 if (!propName.trim().isEmpty()) {
 -                    names.add(propName);
++                    dependencies.add(propName);
+                 }
+             });
+         }
+ 
++        /**
++         * Fallback for all filters not explicitly handled by the setting applied in the constructor.
++         */
+         @Override
 -        protected void typeNotFound(final CodeList<?> type, final Filter<Object> filter, final Collection<String> names) {
 -            for (final Expression<? super Object, ?> f : filter.getExpressions()) {
 -                visit(f, names);
++        protected void typeNotFound(final CodeList<?> type, final Filter<Object> filter, final Collection<String> dependencies) {
++            for (final Expression<Object,?> f : filter.getExpressions()) {
++                visit(f, dependencies);
+             }
+         }
+ 
++        /**
++         * Fallback for all expressions not explicitly handled by the setting applied in the constructor.
++         */
+         @Override
 -        protected void typeNotFound(final String type, final Expression<Object, ?> expression, final Collection<String> names) {
 -            for (final Expression<? super Object, ?> p : expression.getParameters()) {
 -                visit(p, names);
++        protected void typeNotFound(final String type, final Expression<Object,?> expression, final Collection<String> dependencies) {
++            for (final Expression<Object,?> p : expression.getParameters()) {
++                visit(p, dependencies);
+             }
+         }
+     }
+ }
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
index c4ddb1c2b8,c4ddb1c2b8..ad4c132716
--- 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
@@@ -17,6 -17,6 +17,7 @@@
  package org.apache.sis.feature;
  
  import java.util.Map;
++import java.util.function.Function;
  import org.opengis.util.GenericName;
  import org.opengis.util.FactoryException;
  import org.opengis.util.InternationalString;
@@@ -28,9 -28,9 +29,12 @@@ import org.apache.sis.util.collection.W
  import org.apache.sis.util.resources.Errors;
  
  // Branch-dependent imports
++import org.opengis.feature.Feature;
  import org.opengis.feature.Operation;
  import org.opengis.feature.PropertyType;
++import org.opengis.feature.AttributeType;
  import org.opengis.feature.FeatureAssociationRole;
++import org.opengis.filter.Expression;
  
  
  /**
@@@ -107,7 -107,7 +111,7 @@@
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-- * @version 1.0
++ * @version 1.4
   * @since   0.7
   */
  public final class FeatureOperations extends Static {
@@@ -260,4 -260,4 +264,49 @@@
          ArgumentChecks.ensureNonNull("geometryAttributes", geometryAttributes);
          return POOL.unique(new EnvelopeOperation(identification, crs, geometryAttributes));
      }
++
++    /**
++     * Creates an operation which delegates the computation to a given expression.
++     * The {@code expression} argument should generally be an instance of
++     * {@link org.opengis.filter.Expression},
++     * but more generic functions are accepted as well.
++     *
++     * @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.
++     * @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)
++    {
++        ArgumentChecks.ensureNonNull("expression", expression);
++        return POOL.unique(new ExpressionOperation<>(identification, expression, result));
++    }
++
++    /**
++     * 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
++     * parameterized type {@code <V>} between {@code expression} and {@code result} can not be enforced at compile time.
++     * This method casts or converts the expression to the expected type by a call to
++     * {@link Expression#toValueType(Class)}.
++     *
++     * @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.
++     * @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)
++    {
++        return expression(identification, expression.toValueType(result.getValueClass()), result);
++    }
  }
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
index c83f847b61,c83f847b61..e974d358da
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
@@@ -133,7 -133,7 +133,7 @@@ final class LinkOperation extends Abstr
       */
      @Override
      public boolean equals(final Object obj) {
--        // 'this.result' is compared (indirectly) by the super class.
++        // `this.result` is compared (indirectly) by the super class.
          return super.equals(obj) && referentName.equals(((LinkOperation) obj).referentName);
      }
  
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
index 8a1f10aa71,8a1f10aa71..15acc606d4
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
@@@ -177,6 -177,6 +177,7 @@@ public final class AttributeTypeBuilder
       * Sets the {@code AttributeType} name as a generic name.
       * If another name was defined before this method call, that previous value will be discarded.
       *
++     * @param  name  the attribute name (cannot be {@code null}).
       * @return {@code this} for allowing method calls chaining.
       */
      @Override
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
index cc81863b73,cc81863b73..86b379a201
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
@@@ -25,6 -25,6 +25,7 @@@ import org.apache.sis.filter.Optimizati
  import org.apache.sis.filter.DefaultFilterFactory;
  import org.apache.sis.feature.builder.FeatureTypeBuilder;
  import org.apache.sis.feature.builder.PropertyTypeBuilder;
++import org.apache.sis.feature.builder.AttributeTypeBuilder;
  
  
  /**
@@@ -47,6 -47,6 +48,12 @@@ public interface FeatureExpression<R,V
      /**
       * Returns the type of values computed by this expression, or {@code Object.class} if unknown.
       *
++     * <h4>Note on type safety</h4>
++     * The parameterized type should be {@code <? extends V>} because some implementations get this
++     * information by a call to {@code value.getClass()}. But it should also be {@code <? super V>}
++     * for supporting the {@code Object.class} return value. Those contradictory requirements force
++     * us to use {@code <?>}.
++     *
       * @return the type of values computed by this expression.
       */
      default Class<?> getValueClass() {
@@@ -59,6 -59,6 +66,9 @@@
       * {@link AttributeType} or a {@link org.opengis.feature.FeatureAssociationRole}
       * but not an {@link org.opengis.feature.Operation}.
       *
++     * <p>If this method returns an instance of {@link AttributeTypeBuilder}, then its parameterized
++     * type should be the same {@code <V>} than this {@code FeatureExpression}.</p>
++     *
       * @param  valueType  the type of features to be evaluated by the given expression.
       * @param  addTo      where to add the type of properties evaluated by this expression.
       * @return builder of the added property, or {@code null} if this method cannot add a property.
@@@ -81,6 -81,6 +91,10 @@@
       * It is caller's responsibility to verify if this method returns {@code null} and to throw an exception in such case.
       * We leave that responsibility to the caller because (s)he may be able to provide better error messages.
       *
++     * <h4>Note on type safety</h4>
++     * This method does not use parameterized types because of the assumption on {@link ValueReference}.
++     * As of Apache SIS 1.3, we have no way to check if {@code <R>} is for feature instances.
++     *
       * @param  candidate  the expression to cast or copy. Can be null.
       * @return the given expression as a feature expression, or {@code null} if it cannot be casted or converted.
       */
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
index e7409c28ba,ba751928ea..3fdbb0720a
--- 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
@@@ -23,13 -23,14 +23,17 @@@ import java.util.Map
  import java.util.LinkedHashMap;
  import java.util.Objects;
  import java.util.OptionalLong;
++import java.util.function.Function;
  import java.io.Serializable;
  import javax.measure.Quantity;
  import javax.measure.quantity.Length;
  import org.opengis.util.GenericName;
  import org.opengis.geometry.Envelope;
 -import org.apache.sis.feature.ExpressionOperation;
++import org.apache.sis.feature.AbstractOperation;
++import org.apache.sis.feature.FeatureOperations;
  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.feature.AttributeConvention;
  import org.apache.sis.internal.feature.FeatureExpression;
  import org.apache.sis.internal.filter.SortByComparator;
@@@ -45,6 -46,6 +49,9 @@@ import org.apache.sis.util.resources.Vo
  // Branch-dependent imports
  import org.opengis.feature.Feature;
  import org.opengis.feature.FeatureType;
++import org.opengis.feature.Attribute;
++import org.opengis.feature.AttributeType;
++import org.opengis.feature.Operation;
  import org.opengis.filter.FilterFactory;
  import org.opengis.filter.Filter;
  import org.opengis.filter.Expression;
@@@ -65,7 -66,7 +72,7 @@@ import org.opengis.filter.InvalidFilter
   * <ul>
   *   <li>A <cite>selection</cite> is a filter choosing the features instances to include in the subset.
   *       In relational databases, a feature instances are mapped to table rows.</li>
-- *   <li>A <cite>projection</cite> (not to be confused with map projection) is the set of feature property to keep.
++ *   <li>A <cite>projection</cite> (not to be confused with map projection) is the set of feature properties to keep.
   *       In relational databases, feature properties are mapped to table columns.</li>
   * </ul>
   *
@@@ -75,7 -76,7 +82,7 @@@
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
   * @since   1.1
   */
  public class FeatureQuery extends Query implements Cloneable, Serializable {
@@@ -400,19 -401,19 +407,76 @@@
          return linearResolution;
      }
  
++    /**
++     * Whether a property evaluated by a query is computed on the fly or stored.
++     * By default, an expression is evaluated only once for each feature instance,
++     * then the result is stored as a feature {@link Attribute} value.
++     * But the same expression can also be wrapped in a feature {@link Operation}
++     * and evaluated every times that the value is requested.
++     *
++     * <h2>Analogy with relational databases</h2>
++     * The terminology used in this enumeration is close to the one used in relational database.
++     * A <cite>projection</cite> is the set of feature properties to keep in the query results.
++     * The projection may contain <cite>generated columns</cite>, which are specified in SQL by
++     * {@code SQL GENERATED ALWAYS} statement, optionally with {@code STORED} or {@code VIRTUAL}
++     * modifier.
++     *
++     * @version 1.4
++     * @since   1.4
++     */
++    public enum ProjectionType {
++        /**
++         * The expression is evaluated exactly once when a feature instance is created,
++         * 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.
++         */
++        STORED,
++
++        /**
++         * The expression is evaluated every times that the property value is requested.
++         * The feature property type will be {@link Operation}.
++         * This projection type may be preferable to {@link #STORED} in the following circumstances:
++         *
++         * <ul>
++         *   <li>The expression may produce different results every times that it is evaluated.</li>
++         *   <li>The feature property should be a {@linkplain FeatureOperations#link link} to another attribute.</li>
++         *   <li>Potentially expensive computation should be deferred until first needed.</li>
++         *   <li>Computation result should not be stored in order to reduce memory usage.</li>
++         * </ul>
++         *
++         * @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.
++         */
++    }
++
      /**
       * An expression to be retrieved by a {@code Query}, together with the name to assign to it.
++     * {@code NamedExpression} specifies also if the expression should be evaluated exactly once
++     * and its value stored, or evaluated every times that the value is requested.
++     *
++     * <h2>Analogy with relational databases</h2>
++     * A {@code NamedExpression} instance can be understood as the definition of a column in a SQL database table.
       * In relational database terminology, subset of columns is called <cite>projection</cite>.
++     * A projection is specified by a SQL {@code SELECT} statement, which maps to {@code NamedExpression} as below:
++     *
++     * <p>{@code SELECT} {@link #expression} {@code AS} {@link #alias}</p>
++     *
       * Columns can be given to the {@link FeatureQuery#setProjection(NamedExpression[])} method.
       *
--     * @version 1.2
++     * @version 1.4
       * @since   1.1
       */
      public static class NamedExpression implements Serializable {
          /**
           * For cross-version compatibility.
           */
--        private static final long serialVersionUID = -6919525113513842514L;
++        private static final long serialVersionUID = 4547204390645035145L;
  
          /**
           * The literal, value reference or more complex expression to be retrieved by a {@code Query}.
@@@ -428,30 -429,39 +492,36 @@@
          public final GenericName alias;
  
          /**
-          * Creates a new column with the given expression and no name.
 -         * A virtual expression will only exist as an Operation.
 -         * Those are commonly called 'computed fields' and equivalant of
 -         * SQL GENERATED ALWAYS keyword for columns.
++         * Whether the expression result should be stored or evaluated every times that it is requested.
++         * A stored value will exist as a feature {@link Attribute}, while a virtual value will exist as
++         * a feature {@link Operation}. The latter are commonly called "computed fields" and are equivalent
++         * to SQL {@code GENERATED ALWAYS} keyword for columns.
++         *
++         * @since 1.4
+          */
 -        public final boolean virtual;
++        public final ProjectionType type;
+ 
+         /**
 -         * Creates a new column with the given expression and no name.
++         * Creates a new stored column with the given expression and no name.
           *
           * @param expression  the literal, value reference or expression to be retrieved by a {@code Query}.
           */
          public NamedExpression(final Expression<? super Feature, ?> expression) {
--            ArgumentChecks.ensureNonNull("expression", expression);
--            this.expression = expression;
--            this.alias = null;
 -            this.virtual = false;
++            this(expression, (GenericName) null);
          }
  
          /**
-          * Creates a new column with the given expression and the given name.
 -         * Creates a new persistant column with the given expression and the given name.
++         * Creates a new stored column with the given expression and the given name.
           *
           * @param expression  the literal, value reference or expression to be retrieved by a {@code Query}.
           * @param alias       the name to assign to the expression result, or {@code null} if unspecified.
           */
          public NamedExpression(final Expression<? super Feature, ?> expression, final GenericName alias) {
--            ArgumentChecks.ensureNonNull("expression", expression);
--            this.expression = expression;
--            this.alias = alias;
 -            this.virtual = false;
++            this(expression, alias, ProjectionType.STORED);
          }
  
          /**
-          * Creates a new column with the given expression and the given name.
 -         * Creates a new persistant column with the given expression and the given name.
++         * Creates a new stored column with the given expression and the given name.
           * This constructor creates a {@link org.opengis.util.LocalName} from the given string.
           *
           * @param expression  the literal, value reference or expression to be retrieved by a {@code Query}.
@@@ -461,6 -471,21 +531,24 @@@
              ArgumentChecks.ensureNonNull("expression", expression);
              this.expression = expression;
              this.alias = (alias != null) ? Names.createLocalName(null, null, alias) : null;
 -            this.virtual = false;
++            this.type = ProjectionType.STORED;
+         }
+ 
+         /**
 -         * Creates a new column with the given expression and the given name.
++         * Creates a new column with the given expression, the given name and the given projection type.
+          *
+          * @param expression  the literal, value reference or expression to be retrieved by a {@code Query}.
+          * @param alias       the name to assign to the expression result, or {@code null} if unspecified.
 -         * @param virtual     true to create a computed field, an Operation.
++         * @param type        whether to create a feature {@link Attribute} or a feature {@link Operation}.
++         *
++         * @since 1.4
+          */
 -        public NamedExpression(final Expression<? super Feature, ?> expression, final GenericName alias, boolean virtual) {
++        public NamedExpression(final Expression<? super Feature, ?> expression, final GenericName alias, ProjectionType type) {
+             ArgumentChecks.ensureNonNull("expression", expression);
++            ArgumentChecks.ensureNonNull("type", type);
+             this.expression = expression;
+             this.alias = alias;
 -            this.virtual = virtual;
++            this.type  = type;
          }
  
          /**
@@@ -470,7 -495,7 +558,7 @@@
           */
          @Override
          public int hashCode() {
--            return 37 * expression.hashCode() + Objects.hashCode(alias);
++            return 37 * expression.hashCode() + Objects.hashCode(alias) + type.hashCode();
          }
  
          /**
@@@ -486,7 -511,7 +574,7 @@@
              }
              if (obj != null && getClass() == obj.getClass()) {
                  final NamedExpression other = (NamedExpression) obj;
--                return expression.equals(other.expression) && Objects.equals(alias, other.alias);
++                return expression.equals(other.expression) && Objects.equals(alias, other.alias) && type == other.type;
              }
              return false;
          }
@@@ -593,19 -618,20 +681,20 @@@
          int unnamedNumber = 0;          // Sequential number for unnamed expressions.
          Set<String> names = null;       // Names already used, for avoiding collisions.
          final FeatureTypeBuilder ftb = new FeatureTypeBuilder().setName(valueType.getName());
 -        final GenericName[] columnNames = new GenericName[projection.length];
          for (int column = 0; column < projection.length; column++) {
++            final NamedExpression item = projection[column];
              /*
               * For each property, get the expected type (mandatory) and its name (optional).
               * A default name will be computed if no alias were explicitly given by user.
               */
--            GenericName name = projection[column].alias;
--            final Expression<?,?> expression = projection[column].expression;
++            final Expression<? super Feature,?> expression = item.expression;
              final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(expression);
              final PropertyTypeBuilder resultType;
              if (fex == null || (resultType = fex.expectedType(valueType, ftb)) == null) {
                  throw new InvalidFilterValueException(Resources.format(Resources.Keys.InvalidExpression_2,
                              expression.getFunctionName().toInternationalString(), column));
              }
++            GenericName name = item.alias;
              if (name == null) {
                  /*
                   * Build a list of aliases declared by the user, for making sure that we do not collide with them.
@@@ -649,8 -675,26 +738,20 @@@
                  name = Names.createLocalName(null, null, text);
              }
              resultType.setName(name);
 -            columnNames[column] = name;
 -        }
 -
 -        FeatureType featureType = ftb.build();
 -        /*
 -         * Build virtual fields.
 -         * This operation must be done in a second loop because computed fields
 -         * rely on the projected fields only.
 -         */
 -        for (int column = 0; column < projection.length; column++) {
 -            if (projection[column].virtual) {
 -                //make current property virtual
 -                ftb.properties().remove(columnNames[column]);
 -                final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(projection[column].expression);
 -                ftb.addProperty(new ExpressionOperation(columnNames[column], fex, featureType));
++            /*
++             * If the attribute that we just added should be virtual,
++             * replace the attribute by an operation.
++             */
++            if (item.type == ProjectionType.VIRTUAL && 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));
++                }
+             }
          }
 -        featureType = ftb.build();
 -
 -        return featureType;
 +        return ftb.build();
      }
  
      /**
diff --cc storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
index 0286def28a,144c71e22d..ca2dfb0321
--- 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
@@@ -34,6 -35,7 +35,8 @@@ import org.opengis.feature.Feature
  import org.opengis.feature.FeatureType;
  import org.opengis.feature.PropertyType;
  import org.opengis.feature.AttributeType;
+ import org.opengis.feature.Operation;
++import org.opengis.filter.Expression;
  import org.opengis.filter.Filter;
  import org.opengis.filter.FilterFactory;
  import org.opengis.filter.MatchAction;
@@@ -47,7 -49,7 +50,7 @@@ import org.opengis.filter.SortProperty
   * @author  Johann Sorel (Geomatys)
   * @author  Alexis Manin (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-- * @version 1.3
++ * @version 1.4
   * @since   1.0
   */
  public final class FeatureQueryTest extends TestCase {
@@@ -319,4 -321,53 +322,62 @@@
          assertEquals("value1",  2, instance.getPropertyValue("value1"));
          assertEquals("value3", 25, instance.getPropertyValue("value3"));
      }
+ 
++    /**
++     * Shortcut for creating expression for a projection computed on-the-fly.
++     */
++    private static FeatureQuery.NamedExpression virtualProjection(final Expression<? super Feature, ?> expression, final String alias) {
++        return new FeatureQuery.NamedExpression(expression, Names.createLocalName(null, null, alias), FeatureQuery.ProjectionType.VIRTUAL);
++    }
++
+     /**
+      * Verifies the effect of virtual projections.
+      *
+      * @throws DataStoreException if an error occurred while executing the query.
+      */
+     @Test
+     public void testVirtualProjection() throws DataStoreException {
+         final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures();
 -        query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) null),
 -                            new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), Names.createLocalName(null, null, "renamed1"), true),
 -                            new FeatureQuery.NamedExpression(ff.literal("a literal"), Names.createLocalName(null, null, "computed"), true));
++        query.setProjection(
++                new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) null),
++                virtualProjection(ff.property("value1", Integer.class), "renamed1"),
++                virtualProjection(ff.literal("a literal"), "computed"));
+ 
+         // Check result type.
+         final Feature instance = executeAndGetFirst();
+         final FeatureType resultType = instance.getType();
+         assertEquals("Test", resultType.getName().toString());
+         assertEquals(3, resultType.getProperties(true).size());
+         final PropertyType pt1 = resultType.getProperty("value1");
+         final PropertyType pt2 = resultType.getProperty("renamed1");
+         final PropertyType pt3 = resultType.getProperty("computed");
+         assertTrue(pt1 instanceof AttributeType);
+         assertTrue(pt2 instanceof Operation);
+         assertTrue(pt3 instanceof Operation);
+         assertEquals(Integer.class, ((AttributeType) pt1).getValueClass());
+         assertTrue(((Operation) pt2).getResult() instanceof AttributeType);
+         assertTrue(((Operation) pt3).getResult() instanceof AttributeType);
+         assertEquals(Integer.class, ((AttributeType)((Operation) pt2).getResult()).getValueClass());
+         assertEquals(String.class,  ((AttributeType)((Operation) pt3).getResult()).getValueClass());
+ 
+         // Check feature instance.
+         assertEquals(3, instance.getPropertyValue("value1"));
+         assertEquals(3, instance.getPropertyValue("renamed1"));
+         assertEquals("a literal", instance.getPropertyValue("computed"));
+     }
+ 
+     /**
 -     * Verifies a virtual projection on a missing field causes an exception.
++     * Verifies that a virtual projection on a missing field causes an exception.
+      *
+      * @throws DataStoreException if an error occurred while executing the query.
+      */
+     @Test
+     public void testIncorrectVirtualProjection() throws DataStoreException {
+         final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures();
+         query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) null),
 -                            new FeatureQuery.NamedExpression(ff.property("valueMissing", Integer.class), Names.createLocalName(null, null, "renamed1"), true));
++                            virtualProjection(ff.property("valueMissing", Integer.class), "renamed1"));
+ 
 -        assertThrows(DataStoreContentException.class, () -> executeAndGetFirst());
++        DataStoreContentException ex = assertThrows(DataStoreContentException.class, this::executeAndGetFirst);
++        assertNotNull(ex.getMessage());
+     }
  }