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:28 UTC

[sis] branch geoapi-4.0 updated (f6509802be -> 511ec7b89f)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


    from f6509802be `MathTransforms.linear(MathTransform, DirectPosition)` and `tangent(…)` where duplicating functionality. Deprecate the former in favor of the latter.
     add 3704683e32 feat(FeatureQuery): add support for computed fields in query
     new 511ec7b89f Merge remote-tracking branch 'origin/feat/computed-fields' into geoapi-4.0.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../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(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java


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

Posted by de...@apache.org.
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());
+     }
  }