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/29 16:30:03 UTC

[sis] branch master updated (62c8552cf8 -> ff2beab7ba)

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

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


    from 62c8552cf8 Merge branch 'geoapi-3.1'. This is almost only javadoc.
     add 0f4aaa2d4c Move `CoverageCombiner` to public API.
     add 8f686ad270 Update for a change in localization data in Java 20: `Locale.CANADA` become more like US. We fix the tests by using `Locale.CANADA_FRENCH`, which keep the "year/month/day" format.
     add 59b0f4a2a1 Fix a compilation error which was unnoticed before Java 20.
     add e4fc9a54a7 Make `CoverageCombiner` more suitable to public API: - infer `xdim` and `ydim` automatically. - check units of measurement.
     add 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
     add 511ec7b89f Merge remote-tracking branch 'origin/feat/computed-fields' into geoapi-4.0.
     add 94ed08156b Upgrade dependencies.
     add 8d1d6522c4 Merge branch 'geoapi-4.0' into geoapi-3.1.
     new ff2beab7ba Merge branch 'geoapi-3.1'.

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:
 .../{internal => }/coverage/CoverageCombiner.java  | 165 +++++++++++-----
 .../sis/coverage/grid/GridCoverageBuilder.java     |   5 +
 .../org/apache/sis/coverage/grid/GridExtent.java   |  82 +++++++-
 .../apache/sis/feature/ExpressionOperation.java    | 220 +++++++++++++++++++++
 .../org/apache/sis/feature/FeatureOperations.java  |  51 ++++-
 .../java/org/apache/sis/feature/LinkOperation.java |   2 +-
 .../sis/feature/builder/AttributeTypeBuilder.java  |   1 +
 .../java/org/apache/sis/image/ComputedImage.java   |  20 +-
 .../java/org/apache/sis/image/ImageCombiner.java   |  72 +++----
 .../java/org/apache/sis/image/ImageProcessor.java  |   8 +-
 .../java/org/apache/sis/image/Visualization.java   |   2 +-
 .../sis/internal/coverage/SampleDimensions.java    |  36 ++++
 .../sis/internal/coverage/j2d/ImageLayout.java     |  62 +++++-
 .../sis/internal/feature/FeatureExpression.java    |  14 ++
 .../apache/sis/coverage/CoverageCombinerTest.java  |  70 +++++++
 .../apache/sis/coverage/grid/GridExtentTest.java   |  22 ++-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 .../org/apache/sis/portrayal/CanvasFollower.java   |   2 +-
 .../sis/referencing/operation/matrix/Matrices.java |   8 +-
 .../operation/transform/MathTransforms.java        | 209 ++++++++++----------
 .../operation/transform/UnitConversion.java        | 145 ++++++++++++++
 .../operation/transform/MathTransformsTest.java    |  71 +++----
 ...DefinitionTest.java => UnitConversionTest.java} |  42 ++--
 .../sis/test/suite/ReferencingTestSuite.java       |   1 +
 .../org/apache/sis/measure/RangeFormatTest.java    |   4 +-
 .../java/org/apache/sis/measure/RangeTest.java     |   2 +-
 ide-project/NetBeans/nbproject/project.properties  |   2 +-
 pom.xml                                            |  16 +-
 .../apache/sis/internal/sql/feature/Column.java    |   2 +-
 .../internal/storage/WritableResourceSupport.java  |  11 +-
 .../java/org/apache/sis/storage/FeatureQuery.java  | 133 +++++++++++--
 .../org/apache/sis/storage/FeatureQueryTest.java   |  65 +++++-
 32 files changed, 1217 insertions(+), 329 deletions(-)
 rename core/sis-feature/src/main/java/org/apache/sis/{internal => }/coverage/CoverageCombiner.java (63%)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
 create mode 100644 core/sis-feature/src/test/java/org/apache/sis/coverage/CoverageCombinerTest.java
 create mode 100644 core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/UnitConversion.java
 copy core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/{DomainDefinitionTest.java => UnitConversionTest.java} (50%)


[sis] 01/01: Merge branch 'geoapi-3.1'.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sis.git

commit ff2beab7ba1b2d4147cc8afb1d57fb87c64bcb6a
Merge: 62c8552cf8 8d1d6522c4
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Apr 29 15:05:57 2023 +0200

    Merge branch 'geoapi-3.1'.

 .../{internal => }/coverage/CoverageCombiner.java  | 165 +++++++++++-----
 .../sis/coverage/grid/GridCoverageBuilder.java     |   5 +
 .../org/apache/sis/coverage/grid/GridExtent.java   |  82 +++++++-
 .../apache/sis/feature/ExpressionOperation.java    | 220 +++++++++++++++++++++
 .../org/apache/sis/feature/FeatureOperations.java  |  51 ++++-
 .../java/org/apache/sis/feature/LinkOperation.java |   2 +-
 .../sis/feature/builder/AttributeTypeBuilder.java  |   1 +
 .../java/org/apache/sis/image/ComputedImage.java   |  20 +-
 .../java/org/apache/sis/image/ImageCombiner.java   |  72 +++----
 .../java/org/apache/sis/image/ImageProcessor.java  |   8 +-
 .../java/org/apache/sis/image/Visualization.java   |   2 +-
 .../sis/internal/coverage/SampleDimensions.java    |  36 ++++
 .../sis/internal/coverage/j2d/ImageLayout.java     |  62 +++++-
 .../sis/internal/feature/FeatureExpression.java    |  14 ++
 .../apache/sis/coverage/CoverageCombinerTest.java  |  70 +++++++
 .../apache/sis/coverage/grid/GridExtentTest.java   |  22 ++-
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 .../org/apache/sis/portrayal/CanvasFollower.java   |   2 +-
 .../sis/referencing/operation/matrix/Matrices.java |   8 +-
 .../operation/transform/MathTransforms.java        | 209 ++++++++++----------
 .../operation/transform/UnitConversion.java        | 145 ++++++++++++++
 .../operation/transform/MathTransformsTest.java    |  71 +++----
 .../operation/transform/UnitConversionTest.java    |  59 ++++++
 .../sis/test/suite/ReferencingTestSuite.java       |   1 +
 .../org/apache/sis/measure/RangeFormatTest.java    |   4 +-
 .../java/org/apache/sis/measure/RangeTest.java     |   2 +-
 ide-project/NetBeans/nbproject/project.properties  |   2 +-
 pom.xml                                            |  16 +-
 .../apache/sis/internal/sql/feature/Column.java    |   2 +-
 .../internal/storage/WritableResourceSupport.java  |  11 +-
 .../java/org/apache/sis/storage/FeatureQuery.java  | 133 +++++++++++--
 .../org/apache/sis/storage/FeatureQueryTest.java   |  65 +++++-
 32 files changed, 1251 insertions(+), 312 deletions(-)

diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
index 0000000000,78ecc4b7ad..964a1c143b
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,227 +1,220 @@@
+ /*
+  * 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.Map;
+ import java.util.Set;
+ 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.IdentifiedType;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.LogicalOperator;
 -import org.opengis.filter.ValueReference;
++import org.apache.sis.filter.Filter;
++import org.apache.sis.filter.Expression;
++import org.apache.sis.internal.geoapi.filter.LogicalOperator;
++import org.apache.sis.internal.geoapi.filter.ValueReference;
+ 
+ 
+ /**
+  * 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)
+  * @version 1.4
+  * @since   1.4
+  */
+ 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 expression on which to delegate the execution of this operation.
+      */
+     @SuppressWarnings("serial")                         // Not statically typed as serializable.
 -    private final Function<? super Feature, ? extends V> expression;
++    private final Function<? super AbstractFeature, ? extends V> expression;
+ 
+     /**
+      * The type of result of evaluating the expression.
+      */
 -    @SuppressWarnings("serial")                         // Apache SIS implementations are serializable.
 -    private final AttributeType<? super V> result;
++    private final DefaultAttributeType<? super V> result;
+ 
+     /**
+      * 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;
+ 
+     /**
+      * 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)
++                        final Function<? super AbstractFeature, ? extends V> expression,
++                        final DefaultAttributeType<? super V> result)
+     {
+         super(identification);
+         this.expression = 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 PARAMETERS;
+     }
+ 
+     /**
+      * Returns the expected result type.
+      */
+     @Override
 -    public IdentifiedType getResult() {
++    public AbstractIdentifiedType getResult() {
+         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(final Feature feature, ParameterValueGroup parameters) {
 -        final Attribute<? super V> instance = result.newInstance();
++    public Property apply(final AbstractFeature feature, ParameterValueGroup parameters) {
++        final AbstractAttribute<? super V> instance = result.newInstance();
+         instance.setValue(expression.apply(feature));
+         return instance;
+     }
+ 
+     /**
+      * 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);
+     }
+ 
+     /**
+      * 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, dependencies);
+                 }
+             });
+             setExpressionHandler(FunctionNames.ValueReference, (e, dependencies) -> {
+                 final var expression = (ValueReference<Object,?>) e;
+                 final String propName = expression.getXPath();
+                 if (!propName.trim().isEmpty()) {
+                     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> dependencies) {
++        protected void typeNotFound(final Enum<?> 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> 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 7f052ee11b,ad4c132716..3ddd0ff779
--- 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
@@@ -27,6 -28,14 +28,9 @@@ import org.apache.sis.util.Unconvertibl
  import org.apache.sis.util.collection.WeakHashSet;
  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;
++import org.apache.sis.filter.Expression;
+ 
  
  /**
   * A set of predefined operations expecting a {@code Feature} as input and producing an {@code Attribute} as output.
@@@ -267,4 -264,49 +271,49 @@@ public final class FeatureOperations ex
          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)
++    public static <V> AbstractOperation expression(final Map<String,?> identification,
++                                           final Function<? super AbstractFeature, ? extends V> expression,
++                                           final DefaultAttributeType<? 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)
++    public static <V> AbstractOperation expressionToResult(final Map<String,?> identification,
++                                                   final Expression<? super AbstractFeature, ?> expression,
++                                                   final DefaultAttributeType<V> result)
+     {
+         return expression(identification, expression.toValueType(result.getValueClass()), result);
+     }
  }
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
index 402192c939,86b379a201..bc345fff42
--- 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
@@@ -21,9 -25,7 +21,10 @@@ 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;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.internal.geoapi.filter.Literal;
 +import org.apache.sis.internal.geoapi.filter.ValueReference;
  
  
  /**
diff --cc pom.xml
index 44e695b47c,4d4f351707..7fa6eac0a1
--- a/pom.xml
+++ b/pom.xml
@@@ -546,8 -546,8 +546,8 @@@
      <maven.compiler.target>11</maven.compiler.target>
      <sis.plugin.version>${project.version}</sis.plugin.version>
      <sis.non-free.version>1.3</sis.non-free.version>                <!-- Used only if "non-free" profile is activated. -->
-     <javafx.version>19</javafx.version>                             <!-- Used only if "javafx" profile is activated. -->
+     <javafx.version>20.0.1</javafx.version>                         <!-- Used only if "javafx" profile is activated. -->
 -    <geoapi.version>3.1-SNAPSHOT</geoapi.version>
 +    <geoapi.version>3.0.2</geoapi.version>
    </properties>
  
    <profiles>
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
index 9edc48afa9,3fdbb0720a..30be91a062
--- 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
@@@ -43,14 -47,19 +47,15 @@@ import org.apache.sis.util.iso.Names
  import org.apache.sis.util.resources.Vocabulary;
  
  // 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;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.ValueReference;
 -import org.opengis.filter.SortBy;
 -import org.opengis.filter.SortProperty;
 -import org.opengis.filter.InvalidFilterValueException;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
++import org.apache.sis.feature.DefaultAttributeType;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.internal.geoapi.filter.Literal;
 +import org.apache.sis.internal.geoapi.filter.ValueReference;
 +import org.apache.sis.internal.geoapi.filter.SortBy;
 +import org.apache.sis.internal.geoapi.filter.SortProperty;
  
  
  /**
@@@ -436,10 -506,8 +508,8 @@@ public class FeatureQuery extends Quer
           *
           * @param expression  the literal, value reference or expression to be retrieved by a {@code Query}.
           */
 -        public NamedExpression(final Expression<? super Feature, ?> expression) {
 +        public NamedExpression(final Expression<? super AbstractFeature, ?> expression) {
-             ArgumentChecks.ensureNonNull("expression", expression);
-             this.expression = expression;
-             this.alias = null;
+             this(expression, (GenericName) null);
          }
  
          /**
@@@ -448,10 -516,8 +518,8 @@@
           * @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) {
 +        public NamedExpression(final Expression<? super AbstractFeature, ?> expression, final GenericName alias) {
-             ArgumentChecks.ensureNonNull("expression", expression);
-             this.expression = expression;
-             this.alias = alias;
+             this(expression, alias, ProjectionType.STORED);
          }
  
          /**
@@@ -465,6 -531,24 +533,24 @@@
              ArgumentChecks.ensureNonNull("expression", expression);
              this.expression = expression;
              this.alias = (alias != null) ? Names.createLocalName(null, null, alias) : null;
+             this.type = ProjectionType.STORED;
+         }
+ 
+         /**
+          * 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 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, ProjectionType type) {
++        public NamedExpression(final Expression<? super AbstractFeature, ?> expression, final GenericName alias, ProjectionType type) {
+             ArgumentChecks.ensureNonNull("expression", expression);
+             ArgumentChecks.ensureNonNull("type", type);
+             this.expression = expression;
+             this.alias = alias;
+             this.type  = type;
          }
  
          /**
@@@ -602,14 -687,14 +689,14 @@@
               * 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 Expression<? super AbstractFeature,?> 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,
 +                throw new IllegalArgumentException(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.
@@@ -653,6 -738,18 +740,18 @@@
                  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 (item.type == ProjectionType.VIRTUAL && resultType instanceof AttributeTypeBuilder<?>) {
+                 final var ab = (AttributeTypeBuilder<?>) resultType;
 -                final AttributeType<?> storedType = ab.build();
++                final DefaultAttributeType<?> storedType = ab.build();
+                 if (ftb.properties().remove(resultType)) {
+                     final var properties = Map.of(AbstractOperation.NAME_KEY, name);
+                     ftb.addProperty(FeatureOperations.expressionToResult(properties, expression, storedType));
+                 }
+             }
          }
          return ftb.build();
      }
diff --cc storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
index f90b95c094,ca2dfb0321..91886d5b03
--- 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
@@@ -30,10 -31,17 +31,12 @@@ import org.junit.Test
  import static org.junit.Assert.*;
  
  // Branch-dependent imports
 -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;
 -import org.opengis.filter.SortOrder;
 -import org.opengis.filter.SortProperty;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
- import org.apache.sis.feature.AbstractIdentifiedType;
 +import org.apache.sis.feature.DefaultAttributeType;
++import org.apache.sis.feature.AbstractIdentifiedType;
++import org.apache.sis.feature.AbstractOperation;
++import org.apache.sis.filter.Expression;
  
  
  /**
@@@ -301,4 -322,62 +304,62 @@@ public final class FeatureQueryTest ext
          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) {
++    private static FeatureQuery.NamedExpression virtualProjection(final Expression<? super AbstractFeature, ?> 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();
++        final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures();
+         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();
++        final AbstractFeature instance = executeAndGetFirst();
++        final DefaultFeatureType 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());
++        final AbstractIdentifiedType pt1 = resultType.getProperty("value1");
++        final AbstractIdentifiedType pt2 = resultType.getProperty("renamed1");
++        final AbstractIdentifiedType pt3 = resultType.getProperty("computed");
++        assertTrue(pt1 instanceof DefaultAttributeType);
++        assertTrue(pt2 instanceof AbstractOperation);
++        assertTrue(pt3 instanceof AbstractOperation);
++        assertEquals(Integer.class, ((DefaultAttributeType) pt1).getValueClass());
++        assertTrue(((AbstractOperation) pt2).getResult() instanceof DefaultAttributeType);
++        assertTrue(((AbstractOperation) pt3).getResult() instanceof DefaultAttributeType);
++        assertEquals(Integer.class, ((DefaultAttributeType)((AbstractOperation) pt2).getResult()).getValueClass());
++        assertEquals(String.class,  ((DefaultAttributeType)((AbstractOperation) pt3).getResult()).getValueClass());
+ 
+         // Check feature instance.
+         assertEquals(3, instance.getPropertyValue("value1"));
+         assertEquals(3, instance.getPropertyValue("renamed1"));
+         assertEquals("a literal", instance.getPropertyValue("computed"));
+     }
+ 
+     /**
+      * 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();
++        final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures();
+         query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) null),
+                             virtualProjection(ff.property("valueMissing", Integer.class), "renamed1"));
+ 
+         DataStoreContentException ex = assertThrows(DataStoreContentException.class, this::executeAndGetFirst);
+         assertNotNull(ex.getMessage());
+     }
  }