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

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

commit ad3d4f6c2f5a3fb27114d52558eecffba8fea59a
Merge: 09335964ca 0704b92b5e
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Apr 1 19:28:52 2025 +0200

    Merge branch 'geoapi-3.1'.
    https://issues.apache.org/jira/browse/SIS-608

 .../org/apache/sis/feature/AbstractOperation.java  |  18 +
 .../org/apache/sis/feature/EnvelopeOperation.java  |   3 +-
 .../apache/sis/feature/ExpressionOperation.java    |   3 +-
 .../sis/feature/GroupAsPolylineOperation.java      |   3 +-
 .../main/org/apache/sis/feature/LinkOperation.java |   3 +-
 .../apache/sis/feature/StringJoinOperation.java    |   3 +-
 .../org/apache/sis/feature/internal/Resources.java |   5 +
 .../sis/feature/internal/Resources.properties      |   1 +
 .../sis/feature/internal/Resources_fr.properties   |   1 +
 .../sis/feature/privy/AttributeConvention.java     |   6 +
 .../apache/sis/feature/privy/FeatureUtilities.java |  98 ------
 .../apache/sis/filter/BinaryGeometryFilter.java    |   2 +-
 .../main/org/apache/sis/filter/LeafExpression.java |   2 +-
 .../main/org/apache/sis/filter/internal/Node.java  |  24 +-
 .../sis/filter/privy/ListingPropertyVisitor.java   | 134 ++++++++
 .../org/apache/sis/filter/privy/WarningEvent.java  | 127 +++++++
 .../geometry/wrapper/jts/GeometryTransform.java    |  10 +-
 .../org/apache/sis/geometry/wrapper/jts/JTS.java   |  20 +-
 .../main/org/apache/sis/image/ImageProcessor.java  |   4 +-
 .../sis/geometry/wrapper/esri/FactoryTest.java     |   8 +-
 .../apache/sis/geometry/wrapper/jts/JTSTest.java   |   2 +-
 .../org/apache/sis/metadata/sql/privy/Dialect.java |  52 ++-
 .../apache/sis/metadata/sql/privy/SQLBuilder.java  | 116 +++++--
 .../sis/metadata/sql/privy/SQLUtilities.java       |  43 ++-
 .../org/apache/sis/metadata/sql/privy/Syntax.java  |  70 +++-
 .../org/apache/sis/temporal/LenientDateFormat.java |   6 +-
 .../main/org/apache/sis/temporal/TimeMethods.java  |   6 +-
 .../sis/metadata/sql/privy/SQLBuilderTest.java     |  97 ++++++
 .../sis/metadata/sql/privy/SQLUtilitiesTest.java   |  13 +-
 .../main/org/apache/sis/openoffice/CalcAddins.java |   2 +-
 .../coverage/MultiResolutionCoverageLoader.java    |   2 +-
 .../apache/sis/io/wkt/GeodeticObjectParser.java    |  21 +-
 .../sis/referencing/operation/matrix/Matrices.java |  62 +++-
 .../referencing/operation/matrix/MatricesTest.java |  16 +
 .../apache/sis/storage/landsat/MetadataReader.java |  16 +-
 .../geotiff/reader/GridGeometryBuilder.java        |  13 +-
 .../sis/storage/geotiff/writer/GeoEncoder.java     |  85 ++++-
 .../sis/storage/netcdf/base/GridMapping.java       |   2 +-
 .../sis/storage/netcdf/ucar/DecoderWrapper.java    |   3 +-
 .../main/module-info.java                          |   1 +
 .../org/apache/sis/storage/sql/DataAccess.java     |   2 +
 .../main/org/apache/sis/storage/sql/SQLStore.java  |  43 ++-
 .../org/apache/sis/storage/sql/duckdb/DuckDB.java  |  88 +++++
 .../{postgis => duckdb}/ExtendedClauseWriter.java  |  12 +-
 .../sis/storage/sql/duckdb/package-info.java       |  60 ++++
 .../apache/sis/storage/sql/feature/Analyzer.java   | 112 ++++--
 .../org/apache/sis/storage/sql/feature/Column.java |  67 +++-
 .../apache/sis/storage/sql/feature/Database.java   | 191 +++++++++--
 .../sis/storage/sql/feature/FeatureAdapter.java    |   8 +-
 .../sis/storage/sql/feature/FeatureAnalyzer.java   |  12 +-
 .../sis/storage/sql/feature/FeatureIterator.java   |  87 ++++-
 .../sis/storage/sql/feature/FeatureStream.java     |  91 ++++-
 .../sis/storage/sql/feature/GeometryEncoding.java  |  25 +-
 .../sis/storage/sql/feature/GeometryGetter.java    | 103 ++++--
 .../sis/storage/sql/feature/InfoStatements.java    | 104 ++++--
 .../sis/storage/sql/feature/QueryAnalyzer.java     |   4 +-
 .../apache/sis/storage/sql/feature/Relation.java   |  48 ++-
 .../apache/sis/storage/sql/feature/Resources.java  |  15 +
 .../sis/storage/sql/feature/Resources.properties   |   3 +
 .../storage/sql/feature/Resources_fr.properties    |   5 +-
 .../sis/storage/sql/feature/SelectionClause.java   | 249 +++++++++++++-
 .../storage/sql/feature/SelectionClauseWriter.java |  38 +-
 .../sis/storage/sql/feature/SpatialSchema.java     |  73 +++-
 .../org/apache/sis/storage/sql/feature/Table.java  | 147 ++++++--
 .../sis/storage/sql/feature/TableAnalyzer.java     |  61 ++--
 .../sis/storage/sql/feature/TableReference.java    |   2 +-
 .../storage/sql/postgis/ExtendedClauseWriter.java  |   5 +
 .../sis/storage/sql/postgis/ExtendedInfo.java      |  32 +-
 .../sis/storage/sql/postgis/ExtentEstimator.java   |   2 +-
 .../apache/sis/storage/sql/postgis/Postgres.java   |  22 +-
 .../org/apache/sis/storage/sql/SQLStoreTest.java   |  22 +-
 .../storage/sql/feature/GeometryGetterTest.java    |   3 +-
 .../sis/storage/sql/postgis/PostgresTest.java      | 125 ++++++-
 .../sis/storage/sql/postgis/RasterReaderTest.java  |   5 +-
 .../sis/storage/sql/postgis/RasterWriterTest.java  |   4 +-
 .../sis/storage/sql/postgis/SpatialFeatures.sql    |   8 +
 .../apache/sis/io/stream/InternalOptionKey.java    |   2 +-
 .../main/org/apache/sis/storage/FeatureQuery.java  | 237 +++++--------
 .../main/org/apache/sis/storage/FeatureSubset.java |  37 +-
 .../org/apache/sis/storage/StorageConnector.java   |  28 +-
 .../apache/sis/storage/base/FeatureProjection.java | 381 +++++++++++++++++++++
 .../apache/sis/storage/base/MetadataBuilder.java   | 114 ++++--
 .../main/org/apache/sis/storage/csv/Store.java     |   2 +-
 .../org/apache/sis/storage/image/FormatFilter.java |   4 +-
 .../org/apache/sis/storage/image/FormatFinder.java |  67 +---
 .../apache/sis/storage/image/MultiImageStore.java  |   2 +-
 .../apache/sis/storage/image/SingleImageStore.java |   2 +-
 .../apache/sis/storage/image/WorldFileStore.java   |  31 +-
 .../apache/sis/storage/image/WritableStore.java    |   2 +-
 .../org/apache/sis/storage/wkt/StoreFormat.java    |   2 +-
 .../org/apache/sis/storage/FeatureQueryTest.java   |  23 ++
 .../sis/storage/image/SelfConsistencyTest.java     |   2 +-
 .../sis/storage/image/WorldFileStoreTest.java      |  13 +-
 .../services/org.apache.sis.util.ObjectConverter   |  11 +
 .../org/apache/sis/converter/StringConverter.java  |   2 +-
 .../main/org/apache/sis/setup/GeometryLibrary.java |  37 +-
 .../org/apache/sis/util/collection/WeakEntry.java  |   2 +-
 .../apache/sis/util/collection/WeakHashSet.java    |   1 +
 .../org/apache/sis/util/privy/CollectionsExt.java  |  18 +
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 .../apache/sis/storage/geoheif/GeoHeifStore.java   |   2 +-
 .../storage/shapefile/ListingPropertyVisitor.java  |  79 -----
 .../sis/storage/shapefile/ShapefileStore.java      |   7 +-
 .../apache/sis/storage/shapefile/dbf/DBFField.java |  11 +-
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  68 +++-
 .../gui/coverage/MultiResolutionImageLoader.java   |   2 +-
 .../main/org/apache/sis/gui/map/MapCanvas.java     |   6 +-
 .../main/org/apache/sis/gui/map/package-info.java  |   2 +-
 .../org/apache/sis/storage/gdal/FieldAccessor.java |   3 +-
 .../org/apache/sis/storage/gdal/GDALStore.java     |   4 +-
 112 files changed, 3190 insertions(+), 1002 deletions(-)

diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
index 24efcf6322,a498244380..dc0a3b7d8d
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
@@@ -30,7 -31,19 +31,9 @@@ import org.opengis.parameter.GeneralPar
  import org.opengis.parameter.ParameterDescriptorGroup;
  import org.opengis.parameter.ParameterValueGroup;
  import org.apache.sis.util.Classes;
+ import org.apache.sis.metadata.iso.citation.Citations;
+ import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Attribute;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureAssociation;
 -import org.opengis.feature.FeatureOperationException;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.Operation;
 -import org.opengis.feature.Property;
 -
  
  /**
   * Describes the behaviour of a feature type as a function or a method.
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
index f5efb4ae70,9bf239939c..02a3a25491
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
@@@ -21,9 -21,14 +21,8 @@@ import java.util.Map
  import java.io.IOException;
  import org.opengis.parameter.ParameterValueGroup;
  import org.opengis.parameter.ParameterDescriptorGroup;
- import org.apache.sis.feature.privy.FeatureUtilities;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.Property;
 -import org.opengis.feature.PropertyType;
 -
  
  /**
   * A link operation, which is like a redirection or an alias.
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
index caecc6ea92,8afbdfd5d0..2a7b94368f
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
@@@ -179,11 -183,12 +181,12 @@@ public abstract class Node implements S
              final Geometries<G> library, final Expression<R,?> expression)
      {
          if (expression instanceof GeometryConverter<?,?>) {
-             if (library.equals(((GeometryConverter<?,?>) 
expression).library)) {
+             final Geometries<?> other = ((GeometryConverter<?,?>) 
expression).library;
+             if (library.equals(other)) {
                  return (GeometryConverter<R,G>) expression;
-             } else {
-                 throw new IllegalArgumentException();        // TODO: provide 
a message.
              }
 -            throw new InvalidFilterValueException(Resources.format(
++            throw new IllegalArgumentException(Resources.format(
+                     Resources.Keys.MixedGeometryImplementation_2, 
library.library, other.library));
          }
          return new GeometryConverter<>(library, expression);
      }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/ListingPropertyVisitor.java
index 0000000000,4b9e664a31..719d2f7cb7
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/ListingPropertyVisitor.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/ListingPropertyVisitor.java
@@@ -1,0 -1,137 +1,134 @@@
+ /*
+  * 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.filter.privy;
+ 
+ import java.util.HashSet;
+ import java.util.Set;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.CodeList;
 -import org.opengis.filter.BetweenComparisonOperator;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
 -import org.opengis.filter.ComparisonOperatorName;
 -import org.opengis.filter.LikeOperator;
 -import org.opengis.filter.LogicalOperator;
++// Specific to the main branch:
++import org.apache.sis.filter.Filter;
++import org.apache.sis.filter.Expression;
++import org.apache.sis.pending.geoapi.filter.BetweenComparisonOperator;
++import org.apache.sis.pending.geoapi.filter.ValueReference;
++import org.apache.sis.pending.geoapi.filter.ComparisonOperatorName;
++import org.apache.sis.pending.geoapi.filter.LogicalOperator;
+ 
+ 
+ /**
+  * A collector of all attributes required by a filter or an expression.
+  * This visitor collects the XPaths of all {@link ValueReference} found.
+  *
+  * @author Johann Sorel (Geomatys)
+  * @author Martin Desruisseaux (Geomatys)
+  */
+ public final class ListingPropertyVisitor extends Visitor<Object, 
Set<String>> {
+     /**
+      * The unique instance of this visitor.
+      */
+     private static final ListingPropertyVisitor INSTANCE = new 
ListingPropertyVisitor();
+ 
+     /**
+      * Creates the unique instance of this visitor.
+      */
+     private ListingPropertyVisitor() {
+         setLogicalHandlers((f, names) -> {
+             final var filter = (LogicalOperator<Object>) f;
+             for (Filter<Object> child : filter.getOperands()) {
+                 visit(child, names);
+             }
+         });
+         
setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_BETWEEN),
 (f, names) -> {
+             final var 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 var filter = (LikeOperator<Object>) f;
 -            visit(filter.getExpressions().get(0), names);
++            visit(f.getExpressions().get(0), names);
+         });
+         setExpressionHandler(FunctionNames.ValueReference, (e, names) -> {
+             final var expression = (ValueReference<Object,?>) e;
+             names.add(expression.getXPath());
+         });
+     }
+ 
+     /**
+      * Visits all operands of the given filter for listing all value 
references.
+      *
+      * @param  type    the filter type (may be {@code null}).
+      * @param  filter  the filter (may be {@code null}).
+      * @param  xpaths  where to add the XPaths.
+      */
+     @Override
 -    protected void typeNotFound(final CodeList<?> type, final Filter<Object> 
filter, final Set<String> xpaths) {
++    protected void typeNotFound(final Enum<?> type, final Filter<Object> 
filter, final Set<String> xpaths) {
+         for (final var f : filter.getExpressions()) {
+             visit(f, xpaths);
+         }
+     }
+ 
+     /**
+      * Visits all parameters of the given expression for listing all value 
references.
+      *
+      * @param  type        the expression type (may be {@code null}).
+      * @param  expression  the expression (may be {@code null}).
+      * @param  xpaths      where to add the XPaths.
+      */
+     @Override
+     protected void typeNotFound(final String type, final Expression<Object, 
?> expression, final Set<String> xpaths) {
+         for (final var p : expression.getParameters()) {
+             visit(p, xpaths);
+         }
+     }
+ 
+     /**
+      * Returns all XPaths used, directly or indirectly, by the given filter.
+      * The elements in the set are in no particular order.
+      *
+      * @param  filter  the filter for which to get the XPaths. May be {@code 
null}.
+      * @param  xpaths  a pre-allocated collection where to add the XPaths, or 
{@code null} if none.
+      * @return the given collection, or a new one if it was {@code null}, 
with XPaths added.
+      */
+     @SuppressWarnings("unchecked")
+     public static Set<String> xpaths(final Filter<?> filter, Set<String> 
xpaths) {
+         if (xpaths == null) {
+             xpaths = new HashSet<>();
+         }
+         if (filter != null) {
+             INSTANCE.visit((Filter) filter, xpaths);
+         }
+         return xpaths;
+     }
+ 
+     /**
+      * Returns all XPaths used, directly or indirectly, by the given 
expression.
+      * The elements in the set are in no particular order.
+      *
+      * @param  expression  the expression for which to get the XPaths. May be 
{@code null}.
+      * @param  xpaths  a pre-allocated collection where to add the XPaths, or 
{@code null} if none.
+      * @return the given collection, or a new one if it was {@code null}, 
with XPaths added.
+      */
+     @SuppressWarnings("unchecked")
+     public static Set<String> xpaths(final Expression<?,?> expression, 
Set<String> xpaths) {
+         if (xpaths == null) {
+             xpaths = new HashSet<>();
+         }
+         if (expression != null) {
+             INSTANCE.visit((Expression) expression, xpaths);
+         }
+         return xpaths;
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java
index 0000000000,09a6f891d5..a8e83940c6
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java
@@@ -1,0 -1,128 +1,127 @@@
+ /*
+  * 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.filter.privy;
+ 
+ import java.util.Optional;
+ import java.util.function.Consumer;
 -import org.opengis.util.CodeList;
+ import org.opengis.util.ScopedName;
+ import org.apache.sis.filter.internal.Node;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
++// Specific to the main branch:
++import org.apache.sis.filter.Filter;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * A warning emitted during operations on filters or expressions.
+  * This class is a first draft that may move to public API in a future 
version.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  *
+  * @see <a href="https://issues.apache.org/jira/browse/SIS-460";>SIS-460</a>
+  */
+ public final class WarningEvent {
+     /**
+      * Where to send the warning. If the value is {@code null},
+      * then the warning will be logged to a default logger.
+      */
+     public static final ThreadLocal<Consumer<WarningEvent>> LISTENER = new 
ThreadLocal<>();
+ 
+     /**
+      * The filter or expression that produced this warning.
+      */
+     private final Node source;
+ 
+     /**
+      * The exception that occurred.
+      */
+     public final Exception exception;
+ 
+     /**
+      * Creates a new warning.
+      *
+      * @param  source     the filter or expression that produced this warning.
+      * @param  exception  the exception that occurred.
+      */
+     public WarningEvent(final Node source, final Exception exception) {
+         this.source    = source;
+         this.exception = exception;
+     }
+ 
+     /**
+      * If the source is a filter, returns the operator type.
+      * Otherwise, returns an empty value.
+      *
+      * @return the operator type if the source is a filter.
+      */
 -    public Optional<CodeList<?>> getOperatorType() {
++    public Optional<Enum<?>> getOperatorType() {
+         if (source instanceof Filter<?>) {
+             return Optional.of(((Filter<?>) source).getOperatorType());
+         }
+         return Optional.empty();
+     }
+ 
+     /**
+      * If the source is an expression, returns the function name.
+      * Otherwise, returns an empty value.
+      *
+      * @return the function name if the source is an expression.
+      */
+     public Optional<ScopedName> getFunctionName() {
+         if (source instanceof Expression<?,?>) {
+             return Optional.of(((Expression<?,?>) source).getFunctionName());
+         }
+         return Optional.empty();
+     }
+ 
+     /**
+      * If the source is an expression with at least one parameter of the 
given type, returns that parameter.
+      * If there is many parameter assignable to the given type, then the 
first occurrence is returned.
+      * The {@code type} argument is typically {@code Literal.class} or {@code 
ValueReference.class}.
+      *
+      * @param  type  the desired type of the parameter to return.
+      * @return the first parameter of the given type, or empty if none.
+      */
+     @SuppressWarnings("unchecked")
+     public <P extends Expression<?,?>> Optional<P> getParameter(final 
Class<P> type) {
+         if (source instanceof Filter<?>) {
+             for (Expression<?,?> parameter : ((Filter<?>) 
source).getExpressions()) {
+                 if (type.isInstance(parameter)) {
+                     return Optional.of((P) parameter);
+                 }
+             }
+         }
+         if (source instanceof Expression<?,?>) {
+             for (Expression<?,?> parameter : ((Expression<?,?>) 
source).getParameters()) {
+                 if (type.isInstance(parameter)) {
+                     return Optional.of((P) parameter);
+                 }
+             }
+         }
+         return Optional.empty();
+     }
+ 
+     /**
+      * Returns a string representation of the warning for debugging purposes.
+      *
+      * @return a string representation of the warning.
+      */
+     @Override
+     public String toString() {
+         return source.getDisplayName() + ": " + exception.toString();
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/ExtendedClauseWriter.java
index 140192163f,a6232bda80..41956aef67
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/ExtendedClauseWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/ExtendedClauseWriter.java
@@@ -14,12 -14,10 +14,12 @@@
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
- package org.apache.sis.storage.sql.postgis;
+ package org.apache.sis.storage.sql.duckdb;
  
  import org.apache.sis.storage.sql.feature.SelectionClauseWriter;
 -import org.opengis.filter.SpatialOperatorName;
 +
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.filter.SpatialOperatorName;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
index 30b9a8dfce,428b947eac..7ff19f8852
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
@@@ -26,14 -25,14 +25,14 @@@ import java.sql.PreparedStatement
  import java.sql.ResultSet;
  import java.sql.SQLException;
  import org.apache.sis.metadata.sql.privy.SQLBuilder;
- import org.apache.sis.storage.InternalDataStoreException;
+ import org.apache.sis.storage.base.FeatureProjection;
  import org.apache.sis.util.collection.WeakValueHashMap;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.filter.SortOrder;
 -import org.opengis.filter.SortProperty;
 -import org.opengis.filter.SortBy;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.pending.geoapi.filter.SortOrder;
 +import org.apache.sis.pending.geoapi.filter.SortProperty;
 +import org.apache.sis.pending.geoapi.filter.SortBy;
  
  
  /**
@@@ -110,18 -132,29 +132,29 @@@ final class FeatureIterator implements 
       * @param sort        the {@code ORDER BY} clauses, or {@code null} if 
none.
       * @param offset      number of rows to skip in underlying SQL query, or 
≤ 0 for none.
       * @param count       maximum number of rows to return, or ≤ 0 for no 
limit.
+      * @param projection  additional properties to compute, or {@code null} 
if none.
       */
-     FeatureIterator(final Table table, final Connection connection,
-              final boolean distinct, final String filter, final SortBy<? 
super AbstractFeature> sort,
-              final long offset, final long count)
-             throws SQLException, InternalDataStoreException
+     FeatureIterator(final Table           table,
+                     final Connection      connection,
+                     final boolean         distinct,
+                     final SelectionClause selection,
 -                    final SortBy<? super Feature> sort,
++                    final SortBy<? super AbstractFeature> sort,
+                     final long offset,
+                     final long count,
+                     final FeatureProjection projection)
+             throws Exception
      {
          adapter = table.adapter(connection);
-         spatialInformation = table.database.getSpatialSchema().isPresent()
-                 ? table.database.createInfoStatements(connection) : null;
-         String sql = adapter.sql;
+         String sql = adapter.sql;   // Will be completed below with `WHERE` 
clause if needed.
+ 
+         if (table.database.getSpatialSchema().isPresent()) {
+             spatialInformation = 
table.database.createInfoStatements(connection);
+         } else {
+             spatialInformation = null;
+         }
+         final String filter = (selection != null) ? 
selection.query(connection, spatialInformation) : null;
          if (distinct || filter != null || sort != null || offset > 0 || count 
> 0) {
-             final SQLBuilder builder = new 
SQLBuilder(table.database).append(sql);
+             final var builder = new SQLBuilder(table.database).append(sql);
              if (distinct) {
                  builder.insertDistinctAfterSelect();
              }
@@@ -242,9 -284,9 +284,9 @@@
       * @param  all     {@code true} for reading all remaining feature 
instances, or {@code false} for only the next one.
       * @return {@code true} if we have read an instance and {@code all} is 
{@code false} (so there is maybe other instances).
       */
 -    private boolean fetch(final Consumer<? super Feature> action, final 
boolean all) throws Exception {
 +    private boolean fetch(final Consumer<? super AbstractFeature> action, 
final boolean all) throws Exception {
          while (result.next()) {
-             final AbstractFeature feature = 
adapter.createFeature(spatialInformation, result);
 -            Feature feature = adapter.createFeature(spatialInformation, 
result);
++            AbstractFeature feature = 
adapter.createFeature(spatialInformation, result);
              for (int i=0; i < dependencies.length; i++) {
                  WeakValueHashMap<?,Object> instances = null;
                  Object key = null, value = null;
@@@ -292,8 -347,8 +347,8 @@@
       * @param  owner  if the features to fetch are components of another 
feature, that container feature instance.
       * @return the feature as a singleton {@code Feature} or as a {@code 
Collection<Feature>}.
       */
 -    private Object fetchReferenced(final Feature owner) throws Exception {
 -        final var features = new ArrayList<Feature>();
 +    private Object fetchReferenced(final AbstractFeature owner) throws 
Exception {
-         final List<AbstractFeature> features = new ArrayList<>();
++        final var features = new ArrayList<AbstractFeature>();
          try (ResultSet r = statement.executeQuery()) {
              result = r;
              fetch(features::add, true);
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
index 1ad73ac900,c2a226c459..c1dff382eb
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
@@@ -35,12 -39,15 +39,15 @@@ import org.apache.sis.util.privy.String
  import org.apache.sis.util.stream.DeferredStream;
  import org.apache.sis.util.stream.PaginedStream;
  import org.apache.sis.filter.privy.SortByComparator;
+ import org.apache.sis.filter.privy.WarningEvent;
  import org.apache.sis.storage.DataStoreException;
+ import org.apache.sis.storage.base.FeatureProjection;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.SortBy;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Filter;
++import org.apache.sis.filter.Expression;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.pending.geoapi.filter.SortBy;
  
  
  /**
@@@ -177,17 -192,22 +192,22 @@@ final class FeatureStream extends Defer
           * if we have a "F₀ AND F₁ AND F₂" chain, it is possible to have some 
Fₙ as SQL statements and
           * other Fₙ executed in Java code.
           */
-         final Optimization optimization = new Optimization();
-         optimization.setFeatureType(table.featureType);
 -        Stream<Feature> stream = this;
 +        Stream<AbstractFeature> stream = this;
-         for (final Filter<? super AbstractFeature> filter : 
optimization.applyAndDecompose((Filter<? super AbstractFeature>) predicate)) {
-             if (filter == Filter.include()) continue;
-             if (filter == Filter.exclude()) return empty();
-             if (!selection.tryAppend(filterToSQL, filter)) {
-                 // Delegate to Java code all filters that we cannot translate 
to SQL statement.
-                 stream = super.filter(filter);
-                 hasPredicates = true;
+         try {
+             WarningEvent.LISTENER.set(selection);
+             final var optimization = new Optimization();
+             optimization.setFeatureType(table.featureType);
 -            for (final var filter : optimization.applyAndDecompose((Filter<? 
super Feature>) predicate)) {
++            for (final var filter : optimization.applyAndDecompose((Filter<? 
super AbstractFeature>) predicate)) {
+                 if (filter == Filter.include()) continue;
+                 if (filter == Filter.exclude()) return empty();
+                 if (!selection.tryAppend(filterToSQL, filter)) {
+                     // Delegate to Java code all filters that we cannot 
translate to SQL statement.
+                     stream = super.filter(filter);
+                     hasPredicates = true;
+                 }
              }
+         } finally {
+             WarningEvent.LISTENER.remove();
          }
          return stream;
      }
@@@ -289,7 -309,12 +309,12 @@@
       * be optimized.
       */
      @Override
+     @SuppressWarnings("unchecked")
 -    public <R> Stream<R> map(final Function<? super Feature, ? extends R> 
mapper) {
 +    public <R> Stream<R> map(final Function<? super AbstractFeature, ? 
extends R> mapper) {
+         if (projection == null && mapper instanceof FeatureProjection) {
+             projection = (FeatureProjection) mapper;
+             return (Stream) this;
+         }
          return new PaginedStream<>(super.map(mapper), this);
      }
  
@@@ -393,11 -421,35 +421,35 @@@
       * @throws SQLException if an error occurs while executing the SQL 
statement.
       */
      @Override
 -    protected Spliterator<Feature> createSourceIterator() throws Exception {
 +    protected Spliterator<AbstractFeature> createSourceIterator() throws 
Exception {
-         final String filter = (selection != null && !selection.isEmpty()) ? 
selection.toString() : null;
-         selection = null;             // Let the garbage collector do its 
work.
- 
-         lock(table.database.transactionLocks);
+         Table projected = table;
+         FeatureProjection completion = null;
+         if (projection != null) {
 -            final var unhandled = new LinkedHashMap<String, Expression<? 
super Feature, ?>>();
++            final var unhandled = new LinkedHashMap<String, Expression<? 
super AbstractFeature, ?>>();
+             final var reusedNames = new HashSet<String>();
+             projected = new Table(projected, projection, reusedNames, 
unhandled);
+             if (!unhandled.isEmpty()) {
+                 /*
+                  * Some properties may not be handled by the projection. 
Check if the projected feature contains
+                  * enough information for computing missing properties 
without the need for the original feature.
+                  */
+                 Set<String> references = null;
+                 for (var expression : unhandled.values()) {
+                     references = ListingPropertyVisitor.xpaths(expression, 
references);
+                 }
+                 if (references == null || 
reusedNames.containsAll(references)) {
+                     completion = new FeatureProjection(projected.featureType, 
unhandled);
+                 } else {
+                     /*
+                      * Cannot use `projected` because some expressions need 
properties
+                      * available only in the original features.
+                      */
+                     projected = table;
+                     completion = projection;
+                 }
+             }
+         }
+         lock(projected.database.transactionLocks);
          final Connection connection = getConnection();
          setCloseHandler(connection);  // Executed only if `FeatureIterator` 
creation fails, discarded later otherwise.
          makeReadOnly(connection);
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java
index 9fc13b9955,72e55881ba..a5e5c9dbac
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java
@@@ -16,19 -16,34 +16,33 @@@
   */
  package org.apache.sis.storage.sql.feature;
  
+ import java.util.Map;
+ import java.util.AbstractMap;
+ import java.util.List;
+ import java.util.ArrayList;
+ import java.util.Optional;
+ import java.util.logging.Level;
+ import java.util.logging.LogRecord;
+ import java.util.function.Consumer;
+ import java.sql.Connection;
 -import org.opengis.util.CodeList;
  import org.opengis.geometry.Envelope;
- import org.opengis.geometry.Geometry;
  import org.opengis.metadata.extent.GeographicBoundingBox;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
  import org.apache.sis.geometry.GeneralEnvelope;
  import org.apache.sis.geometry.WraparoundMethod;
  import org.apache.sis.geometry.wrapper.Geometries;
  import org.apache.sis.geometry.wrapper.GeometryWrapper;
  import org.apache.sis.metadata.sql.privy.SQLBuilder;
+ import org.apache.sis.filter.privy.WarningEvent;
+ import org.apache.sis.storage.FeatureSet;
+ import org.apache.sis.system.Modules;
+ import org.apache.sis.util.Utilities;
+ import org.apache.sis.util.Workaround;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.ValueReference;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
  
  
  /**
@@@ -60,6 -123,39 +122,39 @@@ public final class SelectionClause exte
      SelectionClause(final Table table) {
          super(table.database);
          this.table = table;
+         parameters = new ArrayList<>();
+     }
+ 
+     /**
+      * Clears any <abbr>CRS</abbr> that was configured by {@code 
acceptColumnCRS(…)}.
+      * This method should be invoked after the caller finished to write a 
spatial function.
+      */
+     final void clearColumnCRS() {
+         columnCRS = null;
+     }
+ 
+     /**
+      * If the referenced column is a geometry or raster column, remembers its 
default coordinate reference system.
+      * This method can be invoked before {@link #appendLiteral(Object)} in 
order to set a default <abbr>CRS</abbr>
+      * for literals that do not declare themselves their <abbr>CRS</abbr>.
+      *
+      * @param  ref  reference to a property to insert in SQL statement.
+      * @return whether the caller needs to stop the check of operands.
+      *
+      * @see #REPLACE_UNSPECIFIED_CRS
+      */
 -    final boolean acceptColumnCRS(final ValueReference<Feature,?> ref) {
++    final boolean acceptColumnCRS(final ValueReference<AbstractFeature,?> 
ref) {
+         final Column c = table.getColumn(ref.getXPath());
+         if (c != null && c.getGeometryType().isPresent()) {
+             final Optional<CoordinateReferenceSystem> crs = c.getDefaultCRS();
+             if (columnCRS == null) {
+                 columnCRS = crs;    // May be empty, which is not the same as 
null for this class.
+             } else if 
(!Utilities.equalsIgnoreMetadata(columnCRS.orElse(null), crs.orElse(null))) {
+                 clearColumnCRS();
+                 return true;
+             }
+         }
+         return false;
      }
  
      /**
@@@ -170,4 -308,90 +307,90 @@@
          }
          return true;
      }
+ 
+     /**
+      * Returns whether an error occurred while writing the <abbr>SQL</abbr> 
statement.
+      * If this method returns {@code true}, then the caller should truncate 
the SQL to
+      * the last length which was known to be valid and fallback on Java code 
for the rest.
+      */
+     final boolean isInvalid() {
+         return isInvalid;
+     }
+ 
+     /**
+      * Declares the SQL as invalid. It does not means that the whole SQL 
needs to be discarded.
+      * The SQL may be truncated to the last point where it was considered 
valid.
+      */
+     final void invalidate() {
+         isInvalid = true;
+     }
+ 
+     /**
+      * Returns the localized resources for warnings and error messages.
+      */
+     private Resources resources() {
+         return Resources.forLocale(table.database.listeners.getLocale());
+     }
+ 
+     /**
+      * Sets the logger, class and method names of the given record, then logs 
it.
+      * This method declares {@link FeatureSet#features(boolean)} as the 
public source of the log.
+      *
+      * @param  record  the record to configure and log.
+      */
+     private void log(final LogRecord record) {
+         record.setSourceClassName(FeatureSet.class.getName());
+         record.setSourceMethodName("features");
+         record.setLoggerName(Modules.SQL);
+         table.database.listeners.warning(record);
+     }
+ 
+     /**
+      * Invoked when a warning occurred during operations on filters or 
expressions.
+      *
+      * @param  event  the warning.
+      */
+     @Override
+     public void accept(final WarningEvent event) {
+         final LogRecord record = resources().getLogRecord(
+                 Level.WARNING,
+                 Resources.Keys.IncompatibleLiteralCRS_2,
 -                event.getOperatorType().map(CodeList::identifier).orElse("?"),
++                event.getOperatorType().map(Enum::name).orElse("?"),
+                 
event.getParameter(ValueReference.class).map(ValueReference<?,?>::getXPath).orElse("?"));
+         record.setThrown(event.exception);
+         log(record);
+     }
+ 
+     /**
+      * Returns the <abbr>SQL</abbr> fragment built by this {@code 
SelectionClause}.
+      * This method completes the information that we deferred until a 
connection is established.
+      *
+      * @param  spatialInformation  a cache of statements for fetching spatial 
information, or {@code null}.
+      * @return the <abbr>SQL</abbr> fragment, or {@code null} if there is no 
{@code WHERE} clause to add.
+      * @throws Exception if an SQL error, parsing error or other error 
occurred.
+      */
+     final String query(final Connection connection, InfoStatements 
spatialInformation) throws Exception {
+         if (isEmpty()) {
+             return null;
+         }
+         boolean close = false;
+         for (int i = parameters.size(); --i >= 0;) {
+             if (spatialInformation == null) {
+                 spatialInformation = 
table.database.createInfoStatements(connection);
+                 close = true;
+             }
+             final var entry = parameters.get(i);
+             final int index = entry.getKey();
+             final int srid  = spatialInformation.findSRID(entry.getValue());
+             buffer.replace(index, index + 1, Integer.toString(srid));
+         }
+         if (close) {
+             /*
+              * We could put this in a `finally` block, but this method is 
already invoked
+              * in a context where the caller will close the connection in 
case of failure.
+              */
+             spatialInformation.close();
+         }
+         return buffer.toString();
+     }
  }
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java
index c7d5e98412,a9e96dd22c..68a464221f
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java
@@@ -228,9 -229,9 +228,9 @@@ public class SelectionClauseWriter exte
       * @return value of {@link SelectionClause#isInvalid} flag, for allowing 
caller to short-circuit.
       */
      @SuppressWarnings("unchecked")
 -    final boolean write(final SelectionClause sql, final Filter<? super 
Feature> filter) {
 -        visit((Filter<Feature>) filter, sql);
 +    final boolean write(final SelectionClause sql, final Filter<? super 
AbstractFeature> filter) {
 +        visit((Filter<AbstractFeature>) filter, sql);
-         return sql.isInvalid;
+         return sql.isInvalid();
      }
  
      /**
@@@ -240,9 -241,9 +240,9 @@@
       * @param  expression  the expression for which to execute an action 
based on its type.
       * @return value of {@link SelectionClause#isInvalid} flag, for allowing 
caller to short-circuit.
       */
 -    private boolean write(final SelectionClause sql, final 
Expression<Feature, ?> expression) {
 +    private boolean write(final SelectionClause sql, final 
Expression<AbstractFeature, ?> expression) {
          visit(expression, sql);
-         return sql.isInvalid;
+         return sql.isInvalid();
      }
  
      /**
@@@ -386,9 -387,10 +386,10 @@@
      /**
       * Appends a function name with an arbitrary number of parameters 
(potentially zero).
       * This method stops immediately if a parameter cannot be expressed in 
SQL, leaving
-      * the trailing part of the SQL in an invalid state.
+      * the trailing part of the SQL in an invalid state. Callers should check 
if this is
+      * the case by invoking {@link SelectionClause#isInvalid()} after this 
method call.
       */
 -    private final class Function implements BiConsumer<Filter<Feature>, 
SelectionClause> {
 +    private final class Function implements 
BiConsumer<Filter<AbstractFeature>, SelectionClause> {
          /** Name the function. */
          final String name;
  
@@@ -397,10 -399,27 +398,27 @@@
              this.name = name;
          }
  
-         /** Writes the function as an SQL statement. */
+         /**
+          * Writes the function as an SQL statement. The function is usually 
spatial (with geometry operands),
+          * but not necessarily. If the given {@code filter} contains geometry 
operands specified as literal,
+          * {@link org.apache.sis.filter.Optimization} should have already 
transformed the literals to the CRS
+          * of the geometry column when those CRS are known. Therefore, it 
should not be needed to perform any
+          * geometry transformation in this method.
+          */
 -        @Override public void accept(final Filter<Feature> filter, final 
SelectionClause sql) {
 +        @Override public void accept(final Filter<AbstractFeature> filter, 
final SelectionClause sql) {
-             sql.append(name);
-             writeParameters(sql, filter.getExpressions(), ", ", false);
+             sql.appendSpatialFunction(name);
 -            final List<Expression<Feature, ?>> expressions = 
filter.getExpressions();
++            final List<Expression<AbstractFeature, ?>> expressions = 
filter.getExpressions();
+             if (SelectionClause.REPLACE_UNSPECIFIED_CRS) {
 -                for (Expression<Feature,?> exp : expressions) {
++                for (Expression<AbstractFeature,?> exp : expressions) {
+                     if (exp instanceof ValueReference<?,?>) {
 -                        if (sql.acceptColumnCRS((ValueReference<Feature,?>) 
exp)) {
++                        if 
(sql.acceptColumnCRS((ValueReference<AbstractFeature,?>) exp)) {
+                             break;
+                         }
+                     }
+                 }
+             }
+             writeParameters(sql, expressions, ", ", false);
+             sql.clearColumnCRS();
          }
      }
  }
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
index e2157aaafb,808c4b80b7..7d53b4d7d6
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
@@@ -36,11 -41,15 +41,13 @@@ import org.apache.sis.util.Exceptions
  import org.apache.sis.util.collection.WeakValueHashMap;
  import org.apache.sis.util.collection.TreeTable;
  import org.apache.sis.util.iso.DefaultNameSpace;
+ import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.InvalidFilterValueException;
 +// Specific to the main branch:
++import org.apache.sis.filter.Expression;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.feature.DefaultAssociationRole;
  
  
  /**
@@@ -184,25 -193,94 +191,94 @@@ final class Table extends AbstractFeatu
  
      /**
       * Creates a new table as a projection (subset of columns) of the given 
table.
+      * The columns to retain, potentially under different names, are 
specified in {@code projection}.
+      * The projection may also contain complex expressions that cannot be 
handled by this constructor.
+      * Such expressions are stored in the {@code unhandled} map.
       *
-      * @todo This constructor is not yet used because it is an unfinished 
work.
-      *       We need to invent some mechanism for using a subset of the 
columns.
-      *       A starting point is {@link 
org.apache.sis.storage.FeatureQuery#expectedType(DefaultFeatureType)}.
+      * @param  table        the source table.
+      * @param  projection   description of the columns to keep.
+      * @param  reusedNames  an initially empty set where to store the names 
of attributes that are not renamed.
+      * @param  unhandled    an initially empty map where to add expressions 
that are not handled by the new table.
 -     * @throws InvalidFilterValueException if there is an error in the 
declaration of property values.
++     * @throws IllegalArgumentException if there is an error in the 
declaration of property values.
       */
-     Table(final Table parent) {
-         super(parent.listeners, false);
-         database = parent.database;
-         query    = parent.query;
-         name     = parent.name;
- 
-         // TODO: filter the columns.
-         primaryKey   = parent.primaryKey;
-         attributes   = parent.attributes;
-         importedKeys = parent.importedKeys;
-         exportedKeys = parent.exportedKeys;
-         featureType  = parent.featureType;
-         hasGeometry  = parent.hasGeometry;
-         hasRaster    = parent.hasRaster;
+     @SuppressWarnings("LocalVariableHidesMemberVariable")
+     Table(final Table source,
+           final FeatureProjection projection,
+           final Set<String> reusedNames,
 -          final Map<String, Expression<? super Feature, ?>> unhandled)
++          final Map<String, Expression<? super AbstractFeature, ?>> unhandled)
+     {
+         super(source.listeners, false);
+         database    = source.database;
+         query       = source.query;
+         name        = source.name;
+         featureType = projection.featureType;
+         /*
+          * Temporary values the fields, before assignment to final fields.
+          * The final number of attributes and foreigner keys may be smaller.
+          */
+         final var attributes   = new Column  [source.attributes  .length];
+         final var importedKeys = new Relation[source.importedKeys.length];
+         final var exportedKeys = new Relation[source.exportedKeys.length];
+         int attributesCount    = 0;
+         int importedKeysCount  = 0;
+         int exportedKeysCount  = 0;
+         boolean hasGeometry    = false;
+         /*
+          * Take all columns that we can put in the `WHERE` clause of the SQL 
statement.
+          * The columns that we cannot take will be declared in the 
`unhandled` bit set.
+          */
+         final List<String> storedProperties = 
projection.getStoredPropertyNames();
+         final int count = storedProperties.size();
+         for (int i=0; i<count; i++) {
+             final String xpath = projection.getSourcePropertyName(i, false);
+             if (xpath == null) {
+                 unhandled.put(storedProperties.get(i), 
projection.getExpression(i));
+                 continue;
+             }
+             final Column column = source.getColumn(xpath);
+             if (column != null) {
+                 hasGeometry |= column.getGeometryType().isPresent();
+                 final String name = storedProperties.get(i);
+                 final Column renamed = column.rename(name);
+                 attributes[attributesCount++] = renamed;
+                 if (renamed == column) {
+                     reusedNames.add(name);
+                 }
+                 continue;
+             }
+             Relation relation = source.getRelation(xpath, false);
+             if (relation != null) {
+                 importedKeys[importedKeysCount++] = relation;
+                 continue;
+             }
+             relation = source.getRelation(xpath, false);
+             if (relation != null) {
+                 exportedKeys[exportedKeysCount++] = relation;
+                 continue;
+             }
 -            throw new 
InvalidFilterValueException(Errors.forLocale(listeners.getLocale())
++            throw new 
IllegalArgumentException(Errors.forLocale(listeners.getLocale())
+                     .getString(Errors.Keys.PropertyNotFound_2, 
source.featureType.getName(), xpath));
+         }
+         /*
+          * Save the columns and foreigner keys that this table handles.
+          * Take the primary key only if we have all the needed columns
+          * and those columns have the same names as in the source table.
+          */
+         this.hasGeometry  = hasGeometry;
+         this.hasRaster    = source.hasRaster;   // Not yet accurately 
determined.
+         this.attributes   = ArraysExt.resize(attributes,   attributesCount);
+         this.importedKeys = ArraysExt.resize(importedKeys, importedKeysCount);
+         this.exportedKeys = ArraysExt.resize(exportedKeys, exportedKeysCount);
+         if (source.primaryKey != null) {
+             for (String column : source.primaryKey.getColumns()) {
+                 final int i = storedProperties.indexOf(column);
+                 if (i < 0 || 
!column.equals(projection.getSourcePropertyName(i, true))) {
+                     primaryKey = null;
+                     return;
+                 }
+             }
+         }
+         this.primaryKey = source.primaryKey;
      }
  
      /**
@@@ -227,7 -305,7 +303,7 @@@
                       * have been set to association names. If 
`ClassCastException` occurs here, it is a bug
                       * in our object constructions.
                       */
-                     final var association = (DefaultAssociationRole) 
featureType.getProperty(relation.propertyName);
 -                    final var association = (FeatureAssociationRole) 
featureType.getProperty(relation.getPropertyName());
++                    final var association = (DefaultAssociationRole) 
featureType.getProperty(relation.getPropertyName());
                      final Table table = 
tables.get(association.getValueType().getName());
                      if (table == null) {
                          throw new 
InternalDataStoreException(association.toString());
diff --cc 
endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
index 3f8bb5a415,989576bb5b..fe55cf6f5c
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
@@@ -172,10 -174,10 +172,10 @@@ public final class SQLStoreTest extend
       * Creates a {@link SQLStore} instance with the specified table as a 
resource, then tests some queries.
       */
      private void testTableQuery(final StorageConnector connector, final 
ResourceDefinition table) throws Exception {
-         try (SimpleFeatureStore store = new SimpleFeatureStore(new 
SQLStoreProvider(), connector, table)) {
+         try (var store = new SimpleFeatureStore(new SQLStoreProvider(), 
connector, table)) {
              verifyFeatureTypes(store);
-             final Map<String,Integer> countryCount = new HashMap<>();
+             final var countryCount = new HashMap<String,Integer>();
 -            try (Stream<Feature> features = 
store.findResource("Cities").features(false)) {
 +            try (Stream<AbstractFeature> features = 
store.findResource("Cities").features(false)) {
                  features.forEach((f) -> verifyContent(f, countryCount));
              }
              assertEquals(Integer.valueOf(2), countryCount.remove("CAN"));
@@@ -285,7 -287,7 +285,7 @@@
           */
          assertEquals(c.countryName, getIndirectPropertyValue(feature, 
"country", "native_name"));
          if (isCanada) {
-             final AbstractFeature f = (AbstractFeature) 
feature.getPropertyValue("country");
 -            final var f = (Feature) feature.getPropertyValue("country");
++            final var f = (AbstractFeature) 
feature.getPropertyValue("country");
              if (canada == null) {
                  canada = f;
              } else {
@@@ -447,9 -451,9 +447,9 @@@
       * @param  cities  a feature set containing all cities defined for the 
test class.
       */
      private void verifyStreamOperations(final FeatureSet cities) throws 
DataStoreException {
 -        try (Stream<Feature> features = cities.features(false)) {
 +        try (Stream<AbstractFeature> features = cities.features(false)) {
-             final AtomicInteger peekCount = new AtomicInteger();
-             final AtomicInteger mapCount  = new AtomicInteger();
+             final var peekCount = new AtomicInteger();
+             final var mapCount  = new AtomicInteger();
              final long actualPopulations = features.peek(f -> 
peekCount.incrementAndGet())
                      .peek(f -> peekCount.incrementAndGet())
                      .map (f -> {mapCount.incrementAndGet(); return f;})
@@@ -483,8 -487,8 +483,8 @@@
                  "SELECT * FROM " + SCHEMA + ".\"Cities\" WHERE \"population\" 
>= 1000000")))
          {
              final FeatureSet cities = store.findResource("LargeCities");
-             final Map<String,Integer> countryCount = new HashMap<>();
+             final var countryCount = new HashMap<String,Integer>();
 -            try (Stream<Feature> features = cities.features(false)) {
 +            try (Stream<AbstractFeature> features = cities.features(false)) {
                  features.forEach((f) -> verifyContent(f, countryCount));
              }
              assertEquals(Integer.valueOf(1), countryCount.remove("CAN"));
@@@ -506,7 -510,8 +506,7 @@@
              /*
               * Add a filter for parks in France.
               */
-             final FeatureQuery query = new FeatureQuery();
+             final var query = new FeatureQuery();
 -            query.setSortBy(FF.sort(FF.property("native_name"), 
SortOrder.DESCENDING));
              query.setSelection(FF.equal(FF.property("country"), 
FF.literal("FRA")));
              query.setProjection(FF.property("native_name"));
              final FeatureSet frenchParks = parks.subset(query);
@@@ -540,8 -545,8 +540,8 @@@
                  "OFFSET 2 ROWS FETCH NEXT 3 ROWS ONLY")))
          {
              final FeatureSet parks = store.findResource("MyQuery");
 -            final FeatureType type = parks.getType();
 -            final var property = (AttributeType<?>) 
TestUtilities.getSingleton(type.getProperties(true));
 +            final DefaultFeatureType type = parks.getType();
-             final DefaultAttributeType<?> property = 
(DefaultAttributeType<?>) TestUtilities.getSingleton(type.getProperties(true));
++            final var property = (DefaultAttributeType<?>) 
TestUtilities.getSingleton(type.getProperties(true));
              assertEquals("title", property.getName().toString(), "Property 
name should be label defined in query");
              assertEquals(String.class, property.getValueClass(), "Attribute 
should be a string");
              assertEquals(0, property.getMinimumOccurs(), "Column should be 
nullable.");
diff --cc 
endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java
index a0c47e6831,f9ed98cc6b..aed3d72300
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java
@@@ -58,8 -67,8 +67,9 @@@ import org.apache.sis.test.TestUtilitie
  import org.apache.sis.metadata.sql.TestDatabase;
  import org.apache.sis.referencing.crs.HardCodedCRS;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.metadata.iso.identification.AbstractIdentification;
  
  
  /**
@@@ -99,6 -108,19 +109,20 @@@ public final class PostgresTest extend
          assertNull  (   version.getRevision());
      }
  
+     /**
+      * Performs some verification of store metadata.
+      *
+      * @param  metadata  the metadata to verify.
+      */
+     private static void validate(final Metadata metadata) {
+         final Identification identification = 
TestUtilities.getSingleton(metadata.getIdentificationInfo());
 -        assertTrue(identification.getSpatialRepresentationTypes().containsAll(
++        var defId = assertInstanceOf(AbstractIdentification.class, 
identification);
++        assertTrue(defId.getSpatialRepresentationTypes().containsAll(
+                 Arrays.asList(SpatialRepresentationType.TEXT_TABLE,
+                               SpatialRepresentationType.VECTOR,
+                               SpatialRepresentationType.GRID)));
+     }
+ 
      /**
       * Tests reading and writing features and rasters.
       *
@@@ -187,6 -207,62 +209,62 @@@
          }
      }
  
+     /**
+      * Tests iterating over all features without filters.
+      * Opportunistically verifies also the bounding boxes of all features.
+      *
+      * @param  resource  the set of all features.
+      * @throws DataStoreException if an error occurred while fetching the 
envelope of feature instances.
+      */
+     private static void testAllFeatures(final FeatureSet resource) throws 
DataStoreException {
+         final Envelope envelope = resource.getEnvelope().get();
+         assertEquals(-72, envelope.getMinimum(0), 1);
+         assertEquals( 43, envelope.getMaximum(1), 1);
 -        try (Stream<Feature> features = resource.features(false)) {
++        try (Stream<AbstractFeature> features = resource.features(false)) {
+             features.forEach(PostgresTest::validate);
+         }
 -        try (Stream<Feature> features = resource.features(false)) {
++        try (Stream<AbstractFeature> features = resource.features(false)) {
+             assertEquals(8, features.count());
+         }
+     }
+ 
+     /**
+      * Tests iterating over features with a filter applied, with our without 
coordinate operation.
+      * The filter is {@code ST_Intersects(geometry, literal)} where the 
literal is a polygon.
+      * The coordinate operation is simply an axis swapping.
+      *
+      * @param  resource   the set of all features.
+      * @param  transform  whether to apply a coordinate operation.
+      * @throws Exception if an error occurred while fetching the envelope of 
feature instances.
+      */
+     private static void testFilteredFeatures(final FeatureSet resource, final 
boolean transform) throws Exception {
+         final Coordinate[] coordinates = {
+             new Coordinate(-72, 42),
+             new Coordinate(-71, 42),
+             new Coordinate(-71, 43),
+             new Coordinate(-72, 43),
+             new Coordinate(-72, 42)
+         };
+         if (transform) {
+             for (Coordinate c : coordinates) {
+                 double swp = c.x;
+                 c.x = c.y;
+                 c.y = swp;
+             }
+         }
+         final Geometry geom = new 
GeometryFactory().createPolygon(coordinates);
+         if (transform) {
+             JTS.setCoordinateReferenceSystem(geom, 
CommonCRS.WGS84.geographic());
+         } else {
+             geom.setSRID(4326);
+         }
+         final var factory = DefaultFilterFactory.forFeatures();
+         final var filter = factory.intersects(factory.property("geom4326"), 
factory.literal(geom));
 -        try (Stream<Feature> features = 
resource.features(false).filter(filter)) {
++        try (Stream<AbstractFeature> features = 
resource.features(false).filter(filter)) {
+             assertEquals(4, features.count());
+         }
+     }
+ 
      /**
       * Invoked for each feature instances for performing some checks on the 
feature.
       * This method performs only a superficial verification of geometries.
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
index 70896c5751,7a961f9af1..5f2d84d995
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
@@@ -40,23 -36,22 +36,19 @@@ import org.apache.sis.filter.privy.Sort
  import org.apache.sis.storage.internal.Resources;
  import org.apache.sis.pending.jdk.JDK19;
  import org.apache.sis.util.ArgumentChecks;
- import org.apache.sis.util.ArraysExt;
  import org.apache.sis.util.CharSequences;
+ import org.apache.sis.util.Emptiable;
  import org.apache.sis.util.iso.Names;
- import org.apache.sis.util.resources.Vocabulary;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -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;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
- import org.apache.sis.feature.DefaultFeatureType;
- import org.apache.sis.feature.DefaultAttributeType;
 +import org.apache.sis.feature.AbstractAttribute;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.pending.geoapi.filter.Literal;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
 +import org.apache.sis.pending.geoapi.filter.SortBy;
 +import org.apache.sis.pending.geoapi.filter.SortProperty;
  
  
  /**
@@@ -142,10 -137,10 +134,10 @@@ public class FeatureQuery extends Quer
       * @see #setSortBy(SortBy)
       */
      @SuppressWarnings("serial")                 // Most SIS implementations 
are serializable.
 -    private SortBy<Feature> sortBy;
 +    private SortBy<AbstractFeature> sortBy;
  
      /**
-      * Hint used by resources to optimize returned features.
+      * Hint used by resources to optimize returned features, or {@code null} 
for full resolution.
       * Different stores make use of vector tiles of different scales.
       * A {@code null} value means to query data at their full resolution.
       *
@@@ -191,15 -220,17 +217,17 @@@
       * delegates to {@link #setProjection(NamedExpression...)}.
       *
       * @param  properties  properties to retrieve, or {@code null} to 
retrieve all properties.
-      * @throws IllegalArgumentException if a property is duplicated.
+      * @throws IllegalArgumentException if the given array is empty of if a 
property is duplicated.
       */
      @SafeVarargs
+     @SuppressWarnings("varargs")
 -    public final void setProjection(final Expression<? super Feature, ?>... 
properties) {
 +    public final void setProjection(final Expression<? super AbstractFeature, 
?>... properties) {
          NamedExpression[] wrappers = null;
          if (properties != null) {
+             ArgumentChecks.ensureNonEmpty("properties", properties);
              wrappers = new NamedExpression[properties.length];
              for (int i=0; i<wrappers.length; i++) {
 -                final Expression<? super Feature, ?> e = properties[i];
 +                final Expression<? super AbstractFeature, ?> e = 
properties[i];
                  ArgumentChecks.ensureNonNullElement("properties", i, e);
                  wrappers[i] = new NamedExpression(e);
              }
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
index a01e984e1a,32159973bd..88f140538c
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
@@@ -24,12 -24,11 +24,11 @@@ import org.apache.sis.storage.base.Meta
  import org.apache.sis.storage.base.StoreUtilities;
  import org.apache.sis.storage.internal.Resources;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.SortBy;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
- import org.apache.sis.filter.Expression;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.pending.geoapi.filter.SortBy;
  
  
  /**
@@@ -86,11 -91,12 +91,12 @@@ final class FeatureSubset extends Abstr
       * Returns a description of properties that are common to all features in 
this dataset.
       */
      @Override
 -    public synchronized FeatureType getType() throws DataStoreException {
 +    public synchronized DefaultFeatureType getType() throws 
DataStoreException {
          if (resultType == null) {
 -            final FeatureType type = source.getType();
 +            final DefaultFeatureType type = source.getType();
              try {
-                 resultType = query.expectedType(type);
+                 projection = FeatureProjection.create(type, 
query.getProjection());
+                 resultType = (projection != null) ? projection.featureType : 
type;
              } catch (IllegalArgumentException e) {
                  throw new 
DataStoreContentException(Resources.forLocale(listeners.getLocale())
                          
.getString(Resources.Keys.CanNotDeriveTypeFromFeature_1, type.getName()), e);
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/FeatureProjection.java
index 0000000000,5a25571e28..f31860f74d
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/FeatureProjection.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/FeatureProjection.java
@@@ -1,0 -1,383 +1,381 @@@
+ /*
+  * 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.storage.base;
+ 
+ import java.util.List;
+ import java.util.Map;
+ import java.util.Set;
+ import java.util.ConcurrentModificationException;
+ import java.util.function.UnaryOperator;
+ import org.opengis.util.GenericName;
 -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.feature.privy.FeatureExpression;
+ import org.apache.sis.filter.privy.XPath;
+ import org.apache.sis.storage.FeatureQuery;
+ import org.apache.sis.storage.internal.Resources;
+ import org.apache.sis.pending.jdk.JDK19;
+ import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.iso.Names;
+ import org.apache.sis.util.logging.Logging;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
+ import org.apache.sis.util.resources.Vocabulary;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyNotFoundException;
 -import org.opengis.filter.InvalidFilterValueException;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
++// Specific to the main branch:
++import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.feature.DefaultFeatureType;
++import org.apache.sis.feature.AbstractOperation;
++import org.apache.sis.filter.Expression;
++import org.apache.sis.pending.geoapi.filter.ValueReference;
+ 
+ 
+ /**
+  * A function applying projections (with "projected" in the <abbr>SQL</abbr> 
database sense) of features.
+  * Given full feature instances in input, this method returns feature 
instances containing only a subset
+  * of the properties. The property values may also be different if they are 
computed by the expressions.
+  *
+  * @author Guilhem Legal (Geomatys)
+  * @author Martin Desruisseaux (Geomatys)
+  */
 -public class FeatureProjection implements UnaryOperator<Feature> {
++public class FeatureProjection implements UnaryOperator<AbstractFeature> {
+     /**
+      * The type of features created by this mapper.
+      */
 -    public final FeatureType featureType;
++    public final DefaultFeatureType featureType;
+ 
+     /**
+      * Names of the properties to be stored in the target features. This is 
inferred from the {@code properties}
+      * given at construction time, retaining only the elements of type {@link 
FeatureQuery.ProjectionType#STORED}.
+      * Properties that are computed on-the-fly from other properties are not 
included in this list.
+      * This array is arbitrarily set to {@code null} if this projection is an 
identity operation.
+      *
+      * <p>When a new {@code Feature} instance needs to be created with a 
subset of the properties
+      * of the original feature instance, usually only the stored properties 
need to be copied.
+      * Other properties computed on-the-fly from stored properties may not 
need copy.</p>
+      *
+      * @see #isIdentity()
+      */
+     private final String[] storedProperties;
+ 
+     /**
+      * Expressions to apply on the source feature for fetching the property 
values of the projected feature.
+      * Shall have the same length as {@link #storedProperties} and the 
properties shall be in the same order.
+      * This array is arbitrarily set to {@code null} if this projection is an 
identity operation.
+      *
+      * @see #isIdentity()
+      */
 -    private final Expression<? super Feature, ?>[] expressions;
++    private final Expression<? super AbstractFeature, ?>[] expressions;
+ 
+     /**
+      * Infers the type of values evaluated by a query when executed on 
features of the given type.
+      * If some expressions have no name, default names are computed as below:
+      *
+      * <ul>
+      *   <li>If the expression is an instance of {@link ValueReference}, the 
name of the
+      *       property referenced by the {@linkplain ValueReference#getXPath() 
XPath}.</li>
+      *   <li>Otherwise the localized string "Unnamed #1" with increasing 
numbers.</li>
+      * </ul>
+      *
+      * <h4>Identity operation</h4>
+      * If the result is a feature type with all the properties of the source 
feature,
+      * with the same property names in the same order, and if the expressions 
are only
+      * fetching the values (no computation), then this method returns {@code 
null} for
+      * meaning that this projection does nothing.
+      *
+      * @param  sourceType  the type of features to be converted to projected 
features.
+      * @param  projection  descriptions of the properties to keep in the 
projected features, or {@code null} if none.
+      * @return a function for projecting the feature instances, or {@code 
null} if none.
+      * @throws IllegalArgumentException if this method can operate only on 
some feature types
+      *         and the given type is not one of them.
 -     * @throws InvalidFilterValueException if this method cannot determine 
the result type of an expression.
++     * @throws IllegalArgumentException if this method cannot determine the 
result type of an expression.
+      *         It may be because that expression is backed by an unsupported 
implementation.
+      */
 -    public static FeatureProjection create(final FeatureType sourceType, 
final FeatureQuery.NamedExpression[] projection) {
++    public static FeatureProjection create(final DefaultFeatureType 
sourceType, final FeatureQuery.NamedExpression[] projection) {
+         if (projection != null) {
+             var fp = new FeatureProjection(sourceType, projection);
+             if (!fp.isIdentity()) {
+                 return fp;
+             }
+         }
+         return null;
+     }
+ 
+     /**
+      * Creates a new feature projection. If some expressions have no name,
+      * default names are computed as described in {@link #create create(…)}.
+      *
+      * <p>Callers shall invoke {@link #isIdentity()} after construction.
+      * This instance shall not be used if it is the identity operation.</p>
+      *
+      * @param  sourceType  the type of features to be converted to projected 
features.
+      * @param  projection  descriptions of the properties to keep in the 
projected features, or {@code null} if none.
+      * @throws IllegalArgumentException if this method can operate only on 
some feature types
+      *         and the given type is not one of them.
 -     * @throws InvalidFilterValueException if this method cannot determine 
the result type of an expression.
++     * @throws IllegalArgumentException if this method cannot determine the 
result type of an expression.
+      *         It may be because that expression is backed by an unsupported 
implementation.
+      */
 -    protected FeatureProjection(final FeatureType sourceType, final 
FeatureQuery.NamedExpression[] projection) {
++    protected FeatureProjection(final DefaultFeatureType sourceType, final 
FeatureQuery.NamedExpression[] projection) {
+         int storedCount   = 0;
+         int unnamedNumber = 0;          // Sequential number for unnamed 
expressions.
+         Set<String> names = null;       // Names already used, for avoiding 
collisions.
+ 
+         // For detecting if the projection would be an identity operation.
+         var propertiesOfIdentity = sourceType.getProperties(true).iterator();
+ 
+         @SuppressWarnings({"LocalVariableHidesMemberVariable", "unchecked", 
"rawtypes"})
 -        final Expression<? super Feature,?>[] expressions = new 
Expression[projection.length];
++        final Expression<? super AbstractFeature,?>[] expressions = new 
Expression[projection.length];
+ 
+         @SuppressWarnings("LocalVariableHidesMemberVariable")
+         final String[] storedProperties = new String[projection.length];
+ 
+         final FeatureTypeBuilder ftb = new 
FeatureTypeBuilder().setName(sourceType.getName());
+         for (int column = 0; column < projection.length; column++) {
+             final FeatureQuery.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 was explicitly 
given by the user.
+              */
+             final var expression = item.expression;
+             final var fex = FeatureExpression.castOrCopy(expression);
+             final PropertyTypeBuilder resultType;
+             if (fex == null || (resultType = fex.expectedType(sourceType, 
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.
+                  * No check for `GenericName` collision here because it was 
already verified by `setProjection(…)`.
+                  * We may have collision of their `String` representations 
however, which is okay.
+                  */
+                 if (names == null) {
+                     names = JDK19.newHashSet(projection.length);
+                     for (final FeatureQuery.NamedExpression p : projection) {
+                         if (p.alias != null) {
+                             names.add(p.alias.toString());
+                         }
+                     }
+                 }
+                 /*
+                  * If the expression is a `ValueReference`, the 
`PropertyType` instance can be taken directly
+                  * from the source feature (the Apache SIS implementation 
does just that). If the name is set,
+                  * then we assume that it is correct. Otherwise we take the 
tip of the XPath.
+                  */
+                 String tip = reference(expression, true);
+                 if (tip != null) {
+                     /*
+                      * Take the existing `GenericName` instance from the 
property. It should be equivalent to
+                      * creating a name from the tip, except that it may be a 
`ScopedName` instead of `LocalName`.
+                      * We do not take `resultType.getName()` because the 
latter is different if the property
+                      * is itself a link to another property (in which case 
`resultType` is the final target).
+                      */
+                     name = sourceType.getProperty(tip).getName();
+                     if (name == null || !names.add(name.toString())) {
+                         name = null;
+                         if (tip.isBlank() || names.contains(tip)) {
+                             tip = null;
+                         }
+                     }
+                 }
+                 /*
+                  * If we still have no name at this point, create a name like 
"Unnamed #1".
+                  * Note that despite the use of `Vocabulary` resources, the 
name will be unlocalized
+                  * (for easier programmatic use) because `GenericName` 
implementation is designed for
+                  * providing localized names only if explicitly requested.
+                  */
+                 if (name == null) {
+                     CharSequence text = tip;
+                     if (text == null) do {
+                         text = 
Vocabulary.formatInternational(Vocabulary.Keys.Unnamed_1, ++unnamedNumber);
+                     } while (!names.add(text.toString()));
+                     name = Names.createLocalName(null, null, text);
+                 }
+             }
+             /*
+              * If the attribute that we just added should be virtual, replace 
the attribute by an operation.
+              * When `FeatureProjection` contains at least one such virtual 
attributes, the result cannot be
+              * an identity operation.
+              */
+             if (item.type != FeatureQuery.ProjectionType.STORED) {
+                 propertiesOfIdentity = null;        // No longer an identity 
operation.
+                 if (resultType instanceof AttributeTypeBuilder<?>) {
+                     final var ab = (AttributeTypeBuilder<?>) resultType;
+                     final var storedType = ab.build();
+                     if (ftb.properties().remove(resultType)) {
+                         final var identification = 
Map.of(AbstractOperation.NAME_KEY, name);
+                         
ftb.addProperty(FeatureOperations.expression(identification, expression, 
storedType));
+                     }
+                 }
+                 continue;
+             }
+             /*
+              * This is the usual case where the property value is copied in 
the "projected" feature.
+              * If the target property name is equal to the source property 
name, in the same order,
+              * and the expression is just fetching the value from a property 
of the same name, then
+              * we consider that the projection is an identity operation.
+              */
+             resultType.setName(name);
+             storedProperties[storedCount] = name.toString();
+             expressions[storedCount++] = expression;
+ isIdentity: if (propertiesOfIdentity != null) {
+                 if (propertiesOfIdentity.hasNext()) {
+                     final var property = propertiesOfIdentity.next();
+                     if (name.equals(property.getName())) {
+                         final String tip = name.tip().toString();
+                         if (tip.equals(reference(expression, true))) try {
+                             if (property.equals(sourceType.getProperty(tip))) 
{
+                                 break isIdentity;   // Continue to consider 
that we may have an identity projection.
+                             }
 -                        } catch (PropertyNotFoundException e) {
++                        } catch (IllegalArgumentException e) {
+                             // It may be because the property name is 
ambiguous.
+                             Logging.ignorableException(StoreUtilities.LOGGER, 
FeatureProjection.class, "create", e);
+                         }
+                     }
+                 }
+                 propertiesOfIdentity = null;
+             }
+         }
+         /*
+          * End of the construction of properties from the given expressions. 
If all properties
+          * are just copying values of properties of the same name from the 
source, returns `null`.
+          */
+         if (propertiesOfIdentity == null || propertiesOfIdentity.hasNext()) {
+             this.featureType      = ftb.build();
+             this.storedProperties = ArraysExt.resize(storedProperties, 
storedCount);
+             this.expressions      = ArraysExt.resize(expressions, 
storedCount);
+         } else {
+             this.featureType      = sourceType;
+             this.storedProperties = null;
+             this.expressions      = null;    // Means "identity operation".
+         }
+     }
+ 
+     /**
+      * Creates a new projection with the given properties.
+      *
+      * @param type        the type of feature instances created by this 
projection.
+      * @param projection  all properties by name, associated to expressions 
for getting values from source features.
+      */
+     @SuppressWarnings({"rawtypes", "unchecked"})
 -    public FeatureProjection(final FeatureType type, final Map<String, 
Expression<? super Feature, ?>> projection) {
++    public FeatureProjection(final DefaultFeatureType type, final Map<String, 
Expression<? super AbstractFeature, ?>> projection) {
+         featureType = type;
+         expressions = new Expression[projection.size()];
+         storedProperties = new String[expressions.length];
+         int i = 0;
+         for (var entry : projection.entrySet()) {
+             storedProperties[i] = entry.getKey();
+             expressions[i++] = entry.getValue();
+         }
+         if (i != expressions.length) {
+             // Should never happen, unless the `Map` has been modified 
concurrently.
+             throw new ConcurrentModificationException();
+         }
+     }
+ 
+     /**
+      * If the given expression is a value reference, returns the tip of the 
referenced property.
+      * Otherwise, returns {@code null}.
+      *
+      * @param  expression the expression from which to get the referenced 
property name.
+      * @param  decode  whether to decode the XPath syntax (e.g. {@code {@code 
"Q{namespace}"}} syntax).
+      * @return the referenced property name, or {@code null} if none.
+      */
 -    private static String reference(final Expression<? super Feature, ?> 
expression, final boolean decode) {
++    private static String reference(final Expression<? super AbstractFeature, 
?> expression, final boolean decode) {
+         if (expression instanceof ValueReference<?,?>) {
+             String xpath = ((ValueReference<?,?>) expression).getXPath();
+             return decode ? XPath.toPropertyName(xpath) : xpath;
+         }
+         return null;
+     }
+ 
+     /**
+      * Returns whether this projection performs no operation.
+      * If this method returns {@code true}, then this instance shall 
<strong>not</strong> be used.
+      *
+      * @return whether this projection performs no operation.
+      */
+     public final boolean isIdentity() {
+         return expressions == null;
+     }
+ 
+     /**
+      * Returns the names of all stored properties. This list may be shorter 
than the list of properties
+      * of the {@linkplain #featureType feature type} if some feature 
properties are computed on-the-fly.
+      *
+      * @return the name of all stored properties.
+      */
+     public final List<String> getStoredPropertyNames() {
+         return UnmodifiableArrayList.wrap(storedProperties);
+     }
+ 
+     /**
+      * Returns the name of a property in the source features.
+      * The given index corresponds to an index in the list returned by {@link 
#getStoredPropertyNames()}.
+      * The return value is often the same name as {@code 
getStoredPropertyNames().get(index)}.
+      *
+      * @param  index   index of the stored property for which to get the name 
in the source feature.
+      * @param  decode  whether to decode the XPath syntax (e.g. {@code {@code 
"Q{namespace}"}} syntax).
+      * @return name in the source features, or {@code null} if the property 
is not a {@link ValueReference}.
+      * @throws NullPointerException if {@link #isIdentity()} is {@code true}.
+      */
+     public final String getSourcePropertyName(final int index, final boolean 
decode) {
+         return reference(expressions[index], decode);
+     }
+ 
+     /**
+      * Returns the expression at the given index.
+      *
+      * @param  index  index of the stored property for which to get the 
expression.
+      * @return expression at the given index.
+      * @throws NullPointerException if {@link #isIdentity()} is {@code true}.
+      */
 -    public final Expression<? super Feature, ?> getExpression(final int 
index) {
++    public final Expression<? super AbstractFeature, ?> getExpression(final 
int index) {
+         return expressions[index];
+     }
+ 
+     /**
+      * Derives a new projected feature instance from the given source.
+      *
+      * @param  source the source feature instance.
+      * @return the "projected" (<abbr>SQL</abbr> database sense) feature 
instance.
+      * @throws NullPointerException if {@link #isIdentity()} is {@code true}.
+      */
+     @Override
 -    public final Feature apply(final Feature source) {
++    public final AbstractFeature apply(final AbstractFeature source) {
+         final var feature = featureType.newInstance();
+         for (int i=0; i < expressions.length; i++) {
+             feature.setPropertyValue(storedProperties[i], 
expressions[i].apply(source));
+         }
+         return feature;
+     }
+ 
+     /**
+      * Applies the expressions with the given feature as both the source and 
the target.
+      * IT can be useful when all expressions are computing values derived 
from other attributes.
+      *
+      * @param  feature  the feature on which to apply the expressions.
+      * @throws NullPointerException if {@link #isIdentity()} is {@code true}.
+      */
 -    public final void applySelf(final Feature feature) {
++    public final void applySelf(final AbstractFeature feature) {
+         for (int i=0; i < expressions.length; i++) {
+             feature.setPropertyValue(storedProperties[i], 
expressions[i].apply(feature));
+         }
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
index 9599d7d43f,2d26e160c4..b62caeb9ec
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
@@@ -106,9 -105,8 +105,8 @@@ import org.apache.sis.coverage.grid.Gri
  import org.apache.sis.pending.jdk.JDK21;
  import org.apache.sis.measure.Units;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.FeatureType;
 +// Specific to the main branch:
- import org.opengis.referencing.ReferenceIdentifier;
 +import org.apache.sis.feature.DefaultFeatureType;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
index d97c6f8206,49a8f0eb12..2e672361f0
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
@@@ -533,9 -514,9 +514,9 @@@ loop:   for (int convention=0;; convent
                      break;
                  }
              }
-             builder.addFormatName(format);                          // Does 
nothing if `format` is null.
+             builder.addFormatName(format);      // Does nothing if `format` 
is null.
              builder.addFormatReaderSIS(WorldFileStoreProvider.NAME);
 -            builder.addResourceScope(ScopeCode.COVERAGE, null);
 +            builder.addResourceScope(ScopeCode.valueOf("COVERAGE"), null);
              builder.addSpatialRepresentation(null, 
getGridGeometry(MAIN_IMAGE), true);
              if (gridGeometry.isDefined(GridGeometry.ENVELOPE)) {
                  builder.addExtent(gridGeometry.getEnvelope(), listeners);
diff --cc 
endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
index 36958d18fd,6bd071f8f3..dc9a8758ee
--- 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
+++ 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
@@@ -33,15 -33,22 +33,16 @@@ import org.junit.jupiter.api.Test
  import static org.junit.jupiter.api.Assertions.*;
  import org.apache.sis.test.TestUtilities;
  import org.apache.sis.test.TestCase;
+ import static org.apache.sis.test.Assertions.assertSetEquals;
  import static org.apache.sis.test.Assertions.assertMessageContains;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.IdentifiedType;
 -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;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.feature.DefaultAttributeType;
 +import org.apache.sis.feature.AbstractIdentifiedType;
 +import org.apache.sis.feature.AbstractOperation;
 +import org.apache.sis.filter.Expression;
  
  
  /**
@@@ -196,9 -230,10 +209,10 @@@ public final class FeatureQueryTest ext
      @Test
      public void testSelection() throws DataStoreException {
          createFeaturesWithAssociation();
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
 +        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
          query.setSelection(ff.equal(ff.property("value1", Integer.class),
 -                                    ff.literal(2), true, MatchAction.ALL));
 +                                    ff.literal(2)));
+         assertXPathsEqual("value1");
          verifyQueryResult(1, 2);
      }
  
@@@ -211,8 -246,9 +225,9 @@@
      @Test
      public void testSelectionThroughAssociation() throws DataStoreException {
          createFeaturesWithAssociation();
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
 +        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
          query.setSelection(ff.equal(ff.property("dependency/value3"), 
ff.literal(18)));
+         assertXPathsEqual("dependency/value3");
          verifyQueryResult(3);
      }
  
@@@ -228,21 -264,22 +243,22 @@@
          query.setProjection(new 
FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) 
null),
                              new 
FeatureQuery.NamedExpression(ff.property("value1", Integer.class), "renamed1"),
                              new FeatureQuery.NamedExpression(ff.literal("a 
literal"), "computed"));
+         assertXPathsEqual("value1");
  
          // 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 AttributeType);
 -        assertTrue(pt3 instanceof AttributeType);
 -        assertEquals(Integer.class, ((AttributeType) pt1).getValueClass());
 -        assertEquals(Integer.class, ((AttributeType) pt2).getValueClass());
 -        assertEquals(String.class,  ((AttributeType) pt3).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 DefaultAttributeType);
 +        assertTrue(pt3 instanceof DefaultAttributeType);
 +        assertEquals(Integer.class, ((DefaultAttributeType) 
pt1).getValueClass());
 +        assertEquals(Integer.class, ((DefaultAttributeType) 
pt2).getValueClass());
 +        assertEquals(String.class,  ((DefaultAttributeType) 
pt3).getValueClass());
  
          // Check feature instance.
          assertEquals(3, instance.getPropertyValue("value1"));
@@@ -259,8 -296,9 +275,9 @@@
      public void testProjectionByNames() throws DataStoreException {
          createFeaturesWithAssociation();
          query.setProjection("value2");
+         assertXPathsEqual("value2");
 -        final Feature instance = executeAndGetFirst();
 -        final PropertyType p = 
TestUtilities.getSingleton(instance.getType().getProperties(true));
 +        final AbstractFeature instance = executeAndGetFirst();
 +        final AbstractIdentifiedType p = 
TestUtilities.getSingleton(instance.getType().getProperties(true));
          assertEquals("value2", p.getName().toString());
      }
  
@@@ -278,9 -316,10 +295,10 @@@
          query.setProjection(
                  ff.add(ff.property("value1", Number.class), ff.literal(1)),
                  ff.add(ff.property("value2", Number.class), ff.literal(1)));
+         assertXPathsEqual("value1", "value2");
          final FeatureSet subset = featureSet.subset(query);
 -        final FeatureType type = subset.getType();
 -        final Iterator<? extends PropertyType> properties = 
type.getProperties(true).iterator();
 +        final DefaultFeatureType type = subset.getType();
 +        final Iterator<? extends AbstractIdentifiedType> properties = 
type.getProperties(true).iterator();
          assertEquals("Unnamed #1", properties.next().getName().toString());
          assertEquals("Unnamed #2", properties.next().getName().toString());
          assertFalse(properties.hasNext());
@@@ -299,21 -338,22 +317,22 @@@
      @Test
      public void testProjectionOfAbstractType() throws DataStoreException {
          createFeaturesWithAssociation();
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
 +        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
          query.setProjection(new 
FeatureQuery.NamedExpression(ff.property("value1"),  (String) null),
                              new 
FeatureQuery.NamedExpression(ff.property("/*/unknown"), "unexpected"));
+         assertXPathsEqual("value1", "/*/unknown");
  
          // 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(2, resultType.getProperties(true).size());
 -        final PropertyType pt1 = resultType.getProperty("value1");
 -        final PropertyType pt2 = resultType.getProperty("unexpected");
 -        assertTrue(pt1 instanceof AttributeType<?>);
 -        assertTrue(pt2 instanceof AttributeType<?>);
 -        assertEquals(Integer.class, ((AttributeType<?>) pt1).getValueClass());
 -        assertEquals(Object.class,  ((AttributeType<?>) pt2).getValueClass());
 +        final AbstractIdentifiedType pt1 = resultType.getProperty("value1");
 +        final AbstractIdentifiedType pt2 = 
resultType.getProperty("unexpected");
 +        assertTrue(pt1 instanceof DefaultAttributeType<?>);
 +        assertTrue(pt2 instanceof DefaultAttributeType<?>);
 +        assertEquals(Integer.class, ((DefaultAttributeType<?>) 
pt1).getValueClass());
 +        assertEquals(Object.class,  ((DefaultAttributeType<?>) 
pt2).getValueClass());
  
          // Check feature property values.
          assertEquals(3,    instance.getPropertyValue("value1"));
@@@ -329,11 -369,12 +348,12 @@@
      @Test
      public void testProjectionThroughAssociation() throws DataStoreException {
          createFeaturesWithAssociation();
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
 +        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
          query.setProjection(new 
FeatureQuery.NamedExpression(ff.property("value1"),  (String) null),
                              new 
FeatureQuery.NamedExpression(ff.property("dependency/value3"), "value3"));
+         assertXPathsEqual("value1", "dependency/value3");
          query.setOffset(2);
 -        final Feature instance = executeAndGetFirst();
 +        final AbstractFeature instance = executeAndGetFirst();
          assertEquals( 2, instance.getPropertyValue("value1"));
          assertEquals(25, instance.getPropertyValue("value3"));
      }
@@@ -348,7 -389,8 +368,8 @@@
      public void testProjectionOfLink() throws DataStoreException {
          createFeatureWithIdentifier();
          query.setProjection(AttributeConvention.IDENTIFIER);
+         assertXPathsEqual(AttributeConvention.IDENTIFIER);
 -        final Feature instance = executeAndGetFirst();
 +        final AbstractFeature instance = executeAndGetFirst();
          assertEquals("id-0", 
instance.getPropertyValue(AttributeConvention.IDENTIFIER));
      }
  
@@@ -372,25 -414,26 +393,26 @@@
                  new FeatureQuery.NamedExpression(ff.property("value1", 
Integer.class), (String) null),
                  virtualProjection(ff.property("value1", Integer.class), 
"renamed1"),
                  virtualProjection(ff.literal("a literal"), "computed"));
+         assertXPathsEqual("value1");
  
          // 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);
 -        final IdentifiedType result2 = ((Operation) pt2).getResult();
 -        final IdentifiedType result3 = ((Operation) pt3).getResult();
 -        assertEquals(Integer.class, ((AttributeType<?>) pt1).getValueClass());
 -        assertTrue(result2 instanceof AttributeType<?>);
 -        assertTrue(result3 instanceof AttributeType<?>);
 -        assertEquals(Integer.class, ((AttributeType<?>) 
result2).getValueClass());
 -        assertEquals(String.class,  ((AttributeType<?>) 
result3).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);
 +        final AbstractIdentifiedType result2 = ((AbstractOperation) 
pt2).getResult();
 +        final AbstractIdentifiedType result3 = ((AbstractOperation) 
pt3).getResult();
 +        assertEquals(Integer.class, ((DefaultAttributeType<?>) 
pt1).getValueClass());
 +        assertTrue(result2 instanceof DefaultAttributeType<?>);
 +        assertTrue(result3 instanceof DefaultAttributeType<?>);
 +        assertEquals(Integer.class, ((DefaultAttributeType<?>) 
result2).getValueClass());
 +        assertEquals(String.class,  ((DefaultAttributeType<?>) 
result3).getValueClass());
  
          // Check feature instance.
          assertEquals(3, instance.getPropertyValue("value1"));
@@@ -411,9 -454,10 +433,10 @@@
      @Test
      public void testIncorrectVirtualProjection() throws DataStoreException {
          createFeaturesWithAssociation();
 -        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"));
+         assertXPathsEqual("value1", "valueMissing");
  
          var exception = assertThrows(DataStoreContentException.class, 
this::executeAndGetFirst);
          assertMessageContains(exception);
diff --cc 
endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/image/WorldFileStoreTest.java
index a5c8cdc1ee,0524750b93..8285116b5b
--- 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/image/WorldFileStoreTest.java
+++ 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/image/WorldFileStoreTest.java
@@@ -100,10 -100,11 +100,9 @@@ public final class WorldFileStoreTest e
               */
              assertEquals("gradient", store.getIdentifier().get().toString());
              final Metadata metadata = store.getMetadata();
 -            final Identification id = 
getSingleton(metadata.getIdentificationInfo());
 -            final String format = 
getSingleton(id.getResourceFormats()).getFormatSpecificationCitation().getTitle().toString();
 -            assertTrue(format.contains("PNG"), format);
 +            final DataIdentification id = (DataIdentification) 
getSingleton(metadata.getIdentificationInfo());
              assertEquals("WGS 84", 
getSingleton(metadata.getReferenceSystemInfo()).getName().getCode());
-             final GeographicBoundingBox bbox = (GeographicBoundingBox)
-                     
getSingleton(getSingleton(id.getExtents()).getGeographicElements());
+             final var bbox = (GeographicBoundingBox) 
getSingleton(getSingleton(id.getExtents()).getGeographicElements());
              assertEquals( -90, bbox.getSouthBoundLatitude());
              assertEquals( +90, bbox.getNorthBoundLatitude());
              assertEquals(-180, bbox.getWestBoundLongitude());

Reply via email to