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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 6cffa03c4c Add the SRID argument when executing a spatial function 
`ST_*` in a spatial database. The reprojection of geometry literal, if needed, 
was already handled by `Optimization`. Add special case where the geometry 
column declares the CRS but the literal does not. Redirect to data store 
listeners the logs that may occur during geometry reprojection.
6cffa03c4c is described below

commit 6cffa03c4cf09898aa66ef2dc573b9a5af20e8de
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Mar 19 17:33:44 2025 +0100

    Add the SRID argument when executing a spatial function `ST_*` in a spatial 
database.
    The reprojection of geometry literal, if needed, was already handled by 
`Optimization`.
    Add special case where the geometry column declares the CRS but the literal 
does not.
    Redirect to data store listeners the logs that may occur during geometry 
reprojection.
---
 .../org/apache/sis/feature/internal/Resources.java |   5 +
 .../sis/feature/internal/Resources.properties      |   1 +
 .../sis/feature/internal/Resources_fr.properties   |   1 +
 .../apache/sis/filter/BinaryGeometryFilter.java    |   2 +-
 .../main/org/apache/sis/filter/LeafExpression.java |   2 +-
 .../main/org/apache/sis/filter/internal/Node.java  |  24 +-
 .../org/apache/sis/filter/privy/WarningEvent.java  | 128 +++++++++++
 .../geometry/wrapper/jts/GeometryTransform.java    |  10 +-
 .../org/apache/sis/geometry/wrapper/jts/JTS.java   |  20 +-
 .../sis/geometry/wrapper/esri/FactoryTest.java     |   8 +-
 .../apache/sis/geometry/wrapper/jts/JTSTest.java   |   2 +-
 .../apache/sis/metadata/sql/privy/SQLBuilder.java  |  57 +++--
 .../org/apache/sis/metadata/sql/privy/Syntax.java  |   6 +-
 .../main/org/apache/sis/openoffice/CalcAddins.java |   2 +-
 .../sis/storage/netcdf/base/GridMapping.java       |   2 +-
 .../org/apache/sis/storage/sql/DataAccess.java     |   2 +
 .../sis/storage/sql/feature/FeatureIterator.java   |  26 ++-
 .../sis/storage/sql/feature/FeatureStream.java     |  41 ++--
 .../sis/storage/sql/feature/InfoStatements.java    |   4 +-
 .../apache/sis/storage/sql/feature/Resources.java  |   5 +
 .../sis/storage/sql/feature/Resources.properties   |   1 +
 .../storage/sql/feature/Resources_fr.properties    |   1 +
 .../sis/storage/sql/feature/SelectionClause.java   | 250 +++++++++++++++++++--
 .../storage/sql/feature/SelectionClauseWriter.java |  30 ++-
 .../org/apache/sis/storage/sql/feature/Table.java  |   2 +-
 .../storage/sql/postgis/ExtendedClauseWriter.java  |   5 +
 .../sis/storage/sql/postgis/ExtentEstimator.java   |   2 +-
 .../storage/sql/feature/GeometryGetterTest.java    |   1 +
 .../sis/storage/sql/postgis/PostgresTest.java      |  98 ++++++--
 .../sis/storage/sql/postgis/SpatialFeatures.sql    |   8 +
 .../org/apache/sis/storage/wkt/StoreFormat.java    |   2 +-
 .../main/org/apache/sis/setup/GeometryLibrary.java |  37 ++-
 .../org/apache/sis/util/collection/WeakEntry.java  |   2 +-
 .../apache/sis/storage/geoheif/GeoHeifStore.java   |   2 +-
 34 files changed, 664 insertions(+), 125 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
index 3bdf50aeb5..c3ad59f00a 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
@@ -336,6 +336,11 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short MismatchedValueClass_3 = 48;
 
+        /**
+         * Mixed geometry implementations from two libraries: {0} and {1}.
+         */
+        public static final short MixedGeometryImplementation_2 = 91;
+
         /**
          * No category for value {0}.
          */
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
index df2a468bba..1aeaa4f3bf 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
@@ -74,6 +74,7 @@ MismatchedPropertyType_1          = Mismatched type for 
\u201c{0}\u201d property
 MismatchedSampleModel             = The two images use different sample models.
 MismatchedTileGrid                = The two images have different tile grid.
 MismatchedValueClass_3            = An attribute for \u2018{1}\u2019 values 
where expected, but the \u201c{0}\u201d attribute specifies values of type 
\u2018{2}\u2019.
+MixedGeometryImplementation_2     = Mixed geometry implementations from two 
libraries: {0} and {1}.
 NoCategoryForValue_1              = No category for value {0}.
 NoNDimensionalSlice_3             = Cannot infer a {0}-dimensional slice from 
the grid envelope. Dimension {1} has {2,number} cells.
 NonLinearInDimensions_1           = non-linear in {0} 
dimension{0,choice,1#|2#s}:
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
index 4dfb9f41af..b2b4f45237 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
@@ -79,6 +79,7 @@ MismatchedPropertyType_1          = Le type de la 
propri\u00e9t\u00e9 \u00ab\u20
 MismatchedSampleModel             = Les deux images disposent les pixels 
diff\u00e9remment.
 MismatchedTileGrid                = Les deux images utilisent des grilles de 
tuiles diff\u00e9rentes.
 MismatchedValueClass_3            = Un attribut pour des valeurs de type 
\u2018{1}\u2019 \u00e9tait attendu, mais l\u2019attribut 
\u00ab\u202f{0}\u202f\u00bb sp\u00e9cifie des valeurs de type \u2018{2}\u2019.
+MixedGeometryImplementation_2     = Les g\u00e9om\u00e9tries m\u00e9langent 
des impl\u00e9mentations de deux biblioth\u00e8ques: {0} et {1}.
 NoCategoryForValue_1              = Aucune cat\u00e9gorie n\u2019est 
d\u00e9finie pour la valeur {0}.
 NoNDimensionalSlice_3             = Ne peut pas inf\u00e9rer une tranche 
\u00e0 {0} dimensions \u00e0 partir de l\u2019enveloppe de la grille. La 
dimension {1} a {2,number} cellules.
 NonLinearInDimensions_1           = non-lin\u00e9aire dans {0} 
dimension{0,choice,1#|2#s}\u00a0:
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
index f35237d4d7..685c3ea900 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
@@ -120,7 +120,7 @@ abstract class BinaryGeometryFilter<R> extends Node 
implements SpatialOperator<R
             if (value != null) {
                 final GeometryWrapper gt = context.transform(value);
                 if (gt != value) {
-                    final Expression<R, GeometryWrapper> tr = new 
LeafExpression.Transformed<>(gt, literal);
+                    final var tr = new LeafExpression.Transformed<R, 
GeometryWrapper>(gt, literal);
                     switch (index) {
                         case 0:  expression1 = tr; break;
                         case 1:  expression2 = tr; break;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
index 6350cc0eb6..308c9adc7c 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
@@ -243,7 +243,7 @@ abstract class LeafExpression<R,V> extends Node implements 
FeatureExpression<R,V
                 try {
                     return original.toValueType(target);
                 } catch (RuntimeException bis) {
-                    final ClassCastException c = new 
ClassCastException(Errors.format(
+                    final var c = new ClassCastException(Errors.format(
                             Errors.Keys.CanNotConvertValue_2, 
getFunctionName(), target));
                     c.initCause(e);
                     c.addSuppressed(bis);
diff --git 
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
index 9602325c2f..8afbdfd5d0 100644
--- 
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
@@ -24,6 +24,7 @@ import java.util.Collections;
 import java.util.function.Predicate;
 import java.util.logging.Logger;
 import java.io.Serializable;
+import java.util.function.Consumer;
 import org.opengis.util.CodeList;
 import org.opengis.util.LocalName;
 import org.opengis.util.ScopedName;
@@ -31,6 +32,7 @@ import org.apache.sis.math.FunctionProperty;
 import org.apache.sis.feature.DefaultAttributeType;
 import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.feature.privy.FeatureExpression;
+import org.apache.sis.filter.privy.WarningEvent;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.util.iso.Names;
@@ -144,7 +146,7 @@ public abstract class Node implements Serializable {
      *
      * @return the name of this function.
      */
-    private Object getDisplayName() {
+    public final Object getDisplayName() {
         if (this instanceof Expression<?,?>) {
             return ((Expression<?,?>) this).getFunctionName();
         } else if (this instanceof Filter<?>) {
@@ -181,11 +183,12 @@ public abstract class Node implements Serializable {
             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 InvalidFilterValueException();        // TODO: 
provide a message.
             }
+            throw new InvalidFilterValueException(Resources.format(
+                    Resources.Keys.MixedGeometryImplementation_2, 
library.library, other.library));
         }
         return new GeometryConverter<>(library, expression);
     }
@@ -389,11 +392,16 @@ public abstract class Node implements Serializable {
      * @see <a href="https://issues.apache.org/jira/browse/SIS-460";>SIS-460</a>
      */
     protected final void warning(final Exception e, final boolean recoverable) 
{
-        final String method = (this instanceof Predicate) ? "test" : "apply";
-        if (recoverable) {
-            Logging.recoverableException(LOGGER, getClass(), method, e);
+        final Consumer<WarningEvent> listener = WarningEvent.LISTENER.get();
+        if (listener != null) {
+            listener.accept(new WarningEvent(this, e));
         } else {
-            Logging.unexpectedException(LOGGER, getClass(), method, e);
+            final String method = (this instanceof Predicate) ? "test" : 
"apply";
+            if (recoverable) {
+                Logging.recoverableException(LOGGER, getClass(), method, e);
+            } else {
+                Logging.unexpectedException(LOGGER, getClass(), method, e);
+            }
         }
     }
 }
diff --git 
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
new file mode 100644
index 0000000000..09a6f891d5
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java
@@ -0,0 +1,128 @@
+/*
+ * 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;
+
+
+/**
+ * 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() {
+        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 --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java
index bf638535c1..2cb88090ec 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java
@@ -104,7 +104,7 @@ public abstract class GeometryTransform {
      * @throws TransformException if an error occurred while transforming a 
geometry.
      */
     public MultiPoint transform(final MultiPoint geom) throws 
TransformException {
-        final Point[] subs = new Point[geom.getNumGeometries()];
+        final var subs = new Point[geom.getNumGeometries()];
         for (int i = 0; i < subs.length; i++) {
             subs[i] = transform((Point) geom.getGeometryN(i));
         }
@@ -133,7 +133,7 @@ public abstract class GeometryTransform {
      * @throws TransformException if an error occurred while transforming a 
geometry.
      */
     public MultiLineString transform(final MultiLineString geom) throws 
TransformException {
-        final LineString[] subs = new LineString[geom.getNumGeometries()];
+        final var subs = new LineString[geom.getNumGeometries()];
         for (int i = 0; i < subs.length; i++) {
             subs[i] = transform((LineString) geom.getGeometryN(i));
         }
@@ -163,7 +163,7 @@ public abstract class GeometryTransform {
      */
     public Polygon transform(final Polygon geom) throws TransformException {
         final LinearRing exterior = transform(geom.getExteriorRing());
-        final LinearRing[] holes = new LinearRing[geom.getNumInteriorRing()];
+        final var holes = new LinearRing[geom.getNumInteriorRing()];
         for (int i = 0; i < holes.length; i++) {
             holes[i] = transform(geom.getInteriorRingN(i));
         }
@@ -179,7 +179,7 @@ public abstract class GeometryTransform {
      * @throws TransformException if an error occurred while transforming a 
geometry.
      */
     public MultiPolygon transform(final MultiPolygon geom) throws 
TransformException {
-        final Polygon[] subs = new Polygon[geom.getNumGeometries()];
+        final var subs = new Polygon[geom.getNumGeometries()];
         for (int i = 0; i < subs.length; i++) {
             subs[i] = transform((Polygon) geom.getGeometryN(i));
         }
@@ -195,7 +195,7 @@ public abstract class GeometryTransform {
      * @throws TransformException if an error occurred while transforming a 
geometry.
      */
     public GeometryCollection transform(final GeometryCollection geom) throws 
TransformException {
-        final Geometry[] subs = new Geometry[geom.getNumGeometries()];
+        final var subs = new Geometry[geom.getNumGeometries()];
         for (int i = 0; i < subs.length; i++) {
             subs[i] = transform(geom.getGeometryN(i));
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
index 36cbe002c1..7201ad9ea3 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
@@ -30,6 +30,8 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.crs.AbstractCRS;
+import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.util.Static;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.logging.Logging;
@@ -124,28 +126,32 @@ public final class JTS extends Static {
             if (userData instanceof CoordinateReferenceSystem) {
                 return (CoordinateReferenceSystem) userData;
             } else if (userData instanceof Map<?,?>) {
-                final Map<?,?> map = (Map<?,?>) userData;
+                final var map = (Map<?,?>) userData;
                 final Object value = map.get(CRS_KEY);
                 if (value instanceof CoordinateReferenceSystem) {
                     return (CoordinateReferenceSystem) value;
                 }
             }
             /*
-             * Fallback on SRID with the assumption that they are EPSG codes.
+             * Fallback on SRID with the assumption that they are EPSG codes 
except for axis order which
+             * is (longitude, latitude). This is the order used in popular 
spatial databases and is also
+             * the order frequently used by other libraries that use JTS.
              *
              * TODO: This is not necessarily EPSG code. We need a plugin 
mechanism for specifying the authority.
              * It may be for example the "spatial_ref_sys" table of a spatial 
database.
              */
             final int srid = source.getSRID();
             if (srid > 0) {
-                return CRS.forCode(Constants.EPSG + ':' + srid);
+                CoordinateReferenceSystem crs = CRS.forCode(Constants.EPSG + 
':' + srid);
+                crs = 
AbstractCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
+                return crs;
             }
         }
         return null;
     }
 
     /**
-     * Sets the Coordinate Reference System (CRS) in the specified geometry. 
This method overwrite any previous
+     * Sets the Coordinate Reference System (CRS) in the specified geometry. 
This method overwrites any previous
      * user data; it should be invoked only when the geometry is known to not 
store any other information.
      * In current Apache SIS usage, this method is invoked only for newly 
created geometries.
      *
@@ -155,7 +161,7 @@ public final class JTS extends Static {
      * @param  target  the geometry where to store coordinate reference system 
information.
      * @param  crs     the CRS to store, or {@code null}.
      */
-    static void setCoordinateReferenceSystem(final Geometry target, final 
CoordinateReferenceSystem crs) {
+    public static void setCoordinateReferenceSystem(final Geometry target, 
final CoordinateReferenceSystem crs) {
         target.setUserData(crs);
         int epsg = 0;
         final Identifier id = IdentifiedObjects.getIdentifier(crs, 
Citations.EPSG);
@@ -207,7 +213,7 @@ public final class JTS extends Static {
             bbox = new DefaultGeographicBoundingBox();
             try {
                 final Envelope e = areaOfInterest.getEnvelopeInternal();
-                final GeneralEnvelope env = new GeneralEnvelope(sourceCRS);    
 // May be 3- or 4-dimensional.
+                final var env = new GeneralEnvelope(sourceCRS);     // May be 
3- or 4-dimensional.
                 env.setRange(0, e.getMinX(), e.getMaxX());
                 env.setRange(1, e.getMinY(), e.getMaxY());
                 bbox.setBounds(env);
@@ -296,7 +302,7 @@ public final class JTS extends Static {
      */
     public static Geometry transform(Geometry geometry, final MathTransform 
transform) throws TransformException {
         if (geometry != null && transform != null && !transform.isIdentity()) {
-            final GeometryCoordinateTransform gct = new 
GeometryCoordinateTransform(transform, geometry.getFactory());
+            final var gct = new GeometryCoordinateTransform(transform, 
geometry.getFactory());
             geometry = gct.transform(geometry);
         }
         return geometry;
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/esri/FactoryTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/esri/FactoryTest.java
index eaf53fa485..dfd1c6096b 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/esri/FactoryTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/esri/FactoryTest.java
@@ -82,7 +82,7 @@ public final class FactoryTest extends GeometriesTestCase {
     @Override
     public void testCreatePolyline() {
         super.testCreatePolyline();
-        final Polyline poly = (Polyline) geometry;
+        final var poly = (Polyline) geometry;
         assertEquals(2, poly.getPathCount());
     }
 
@@ -93,7 +93,7 @@ public final class FactoryTest extends GeometriesTestCase {
     @Override
     public void testMergePolylines() {
         super.testMergePolylines();
-        final Polyline poly = (Polyline) geometry;
+        final var poly = (Polyline) geometry;
         assertEquals(3, poly.getPathCount());
     }
 
@@ -105,7 +105,7 @@ public final class FactoryTest extends GeometriesTestCase {
     @Override
     protected void assertWktEquals(String expected, final String actual) {
         assertTrue(actual.startsWith("MULTI"));
-        final StringBuilder b = new StringBuilder(expected.length() + 
7).append("MULTI").append(expected);
+        final var b = new StringBuilder(expected.length() + 
7).append("MULTI").append(expected);
         StringBuilders.replace(b, "(", "((");
         StringBuilders.replace(b, ")", "))");
         expected = b.toString();
@@ -121,6 +121,6 @@ public final class FactoryTest extends GeometriesTestCase {
         final GeometryWrapper ogw = other.castOrWrap(other.createPoint(5, 6));
         assertNotNull(other.getGeometry(ogw));
         var e = assertThrows(ClassCastException.class, () -> 
factory.getGeometry(ogw));
-        assertMessageContains(e, "ESRI", "JAVA2D");
+        assertMessageContains(e, "ESRI", "Java2D");
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
index 6c488a1238..8aec3d6a9e 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
@@ -79,7 +79,7 @@ public final class JTSTest extends TestCase {
          */
         geometry.setUserData(null);
         geometry.setSRID(4326);
-        assertEquals(CommonCRS.WGS84.geographic(), 
JTS.getCoordinateReferenceSystem(geometry));
+        assertEquals(CommonCRS.WGS84.normalizedGeographic(), 
JTS.getCoordinateReferenceSystem(geometry));
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
index e914f49685..dba6153af5 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
@@ -159,55 +159,66 @@ public class SQLBuilder extends Syntax {
     /**
      * Appends an identifier between quote characters.
      *
-     * @param  identifier  the identifier to append.
+     * @param  name  the identifier to append.
      * @return this builder, for method call chaining.
      */
-    public final SQLBuilder appendIdentifier(final String identifier) {
-        buffer.append(quote).append(identifier).append(quote);
+    public final SQLBuilder appendIdentifier(final String name) {
+        buffer.append(quote).append(name).append(quote);
         return this;
     }
 
     /**
      * Appends an identifier for an element in the given schema.
+     * The following rules apply:
      * <ul>
      *   <li>The given schema will be written only if non-null.</li>
-     *   <li>The given schema will be quoted only if {@code quoteSchema} is 
{@code true}.</li>
-     *   <li>The given identifier is always quoted.</li>
+     *   <li>The given schema will be quoted only if {@link #quoteSchema} is 
{@code true}.</li>
+     *   <li>The given name is always quoted.</li>
      * </ul>
      *
-     * @param  schema      the schema, or {@code null} or empty if none.
-     * @param  identifier  the identifier to append.
+     * @param  schema  the schema, or {@code null} or empty if none.
+     * @param  name    the name part of the identifier to append.
      * @return this builder, for method call chaining.
      */
-    public final SQLBuilder appendIdentifier(final String schema, final String 
identifier) {
-        if (schema != null && !schema.isEmpty()) {
-            if (quoteSchema) {
-                appendIdentifier(schema);
-            } else {
-                buffer.append(schema);
-            }
-            buffer.append('.');
-        }
-        return appendIdentifier(identifier);
+    public final SQLBuilder appendIdentifier(final String schema, final String 
name) {
+        return appendIdentifier(null, schema, name, true);
     }
 
     /**
      * Appends an identifier for an element in the given schema and catalog.
+     * The schema is quoted only if {@link #quoteSchema} is {@code true}.
+     * The name part is quoted only if {@code quoteName} is {@code true}.
+     * Unquoted names are useful when the name is for built-in functions,
+     * which often use the lower/upper case convention of the database.
      *
-     * @param  catalog     the catalog, or {@code null} or empty if none.
-     * @param  schema      the schema, or {@code null} or empty if none.
-     * @param  identifier  the identifier to append.
+     * @param  catalog    the catalog, or {@code null} or empty if none.
+     * @param  schema     the schema, or {@code null} or empty if none.
+     * @param  name       the name part of the identifier to append.
+     * @param  quoteName  whether to quote the name part.
      * @return this builder, for method call chaining.
      */
-    public final SQLBuilder appendIdentifier(final String catalog, final 
String schema, final String identifier) {
+    public final SQLBuilder appendIdentifier(final String catalog, final 
String schema, final String name, final boolean quoteName) {
         if (catalog != null && !catalog.isEmpty()) {
             appendIdentifier(catalog);
             buffer.append('.');
-            if (schema == null) {
+            if (schema == null || schema.isEmpty()) {
                 buffer.append(quote).append(quote).append('.');
             }
         }
-        return appendIdentifier(schema, identifier);
+        if (schema != null && !schema.isEmpty()) {
+            if (quoteSchema) {
+                appendIdentifier(schema);
+            } else {
+                buffer.append(schema);
+            }
+            buffer.append('.');
+        }
+        if (quoteName) {
+            return appendIdentifier(name);
+        } else {
+            buffer.append(name);
+            return this;
+        }
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
index 3db59a8239..335aa2f276 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
@@ -41,7 +41,11 @@ public class Syntax {
 
     /**
      * Whether the schema name should be written between quotes. If {@code 
false},
-     * we will let the database engine uses its default lower case / upper 
case policy.
+     * Apache SIS lets the database engine uses its default lower case / upper 
case policy.
+     * This flag is usually {@code true} when the schema was specified by the 
user or has
+     * been discovered from database metadata. This flag is {@code false} when 
the schema
+     * has been created by an Apache SIS script, which intentionally uses 
unquoted schema
+     * for integration with database conventions.
      *
      * @see SQLBuilder#appendIdentifier(String, String)
      */
diff --git 
a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/CalcAddins.java
 
b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/CalcAddins.java
index f98133f25d..d597cf0335 100644
--- 
a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/CalcAddins.java
+++ 
b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/CalcAddins.java
@@ -190,7 +190,7 @@ public abstract class CalcAddins extends WeakBase 
implements XServiceName, XServ
         final Logger logger = getLogger();
         final LogRecord record = new LogRecord(Level.WARNING, 
getLocalizedMessage(exception));
         record.setLoggerName(logger.getName());
-        record.setSourceClassName(getClass().getName());
+        record.setSourceClassName(getClass().getCanonicalName());
         record.setSourceMethodName(method);
         record.setThrown(exception);
         logger.log(record);
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
index 70412a7534..45015c40de 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
@@ -574,7 +574,7 @@ final class GridMapping {
         if (warnings != null) {
             final var record = new LogRecord(Level.WARNING, 
warnings.toString());
             record.setLoggerName(Modules.NETCDF);
-            record.setSourceClassName(Variable.class.getCanonicalName());
+            record.setSourceClassName(Variable.class.getName());
             record.setSourceMethodName("getGridGeometry");
             mapping.decoder.listeners.warning(record);
         }
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java
index 1b7aeee962..bed8c9d194 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java
@@ -90,6 +90,8 @@ public class DataAccess implements AutoCloseable {
     /**
      * Helper methods for fetching information such as coordinate reference 
systems.
      * Created when first needed.
+     *
+     * @see #spatialInformation()
      */
     private InfoStatements spatialInformation;
 
diff --git 
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
index 13be593b51..450f625a5b 100644
--- 
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
@@ -25,7 +25,6 @@ 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.util.collection.WeakValueHashMap;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -109,20 +108,29 @@ final class FeatureIterator implements 
Spliterator<Feature>, AutoCloseable {
      * @param table       the source table.
      * @param connection  connection to the database, used for creating the 
statement.
      * @param distinct    whether the set should contain distinct feature 
instances.
-     * @param filter      condition to append, not including the {@code WHERE} 
keyword.
+     * @param selection   condition to append, not including the {@code WHERE} 
keyword.
      * @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.
      */
-    FeatureIterator(final Table table, final Connection connection,
-             final boolean distinct, final String filter, final SortBy<? super 
Feature> 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 long offset,
+                    final long count)
+            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 var builder = new SQLBuilder(table.database).append(sql);
             if (distinct) {
diff --git 
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
index c24daa5e88..1cbd99ae93 100644
--- 
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,6 +35,7 @@ import org.apache.sis.util.privy.Strings;
 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;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -177,17 +178,22 @@ final class FeatureStream extends DeferredStream<Feature> 
{
          * 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;
-        for (final Filter<? super Feature> filter : 
optimization.applyAndDecompose((Filter<? super Feature>) 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 Filter<? super Feature> filter : 
optimization.applyAndDecompose((Filter<? super Feature>) 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;
     }
@@ -323,12 +329,15 @@ final class FeatureStream extends DeferredStream<Feature> 
{
             sql.appendIdentifier(table.attributes[0].label);
         }
         table.appendFromClause(sql.append(')'));
-        if (selection != null && !selection.isEmpty()) {
-            sql.append(" WHERE ").append(selection.toString());
-        }
         lock(table.database.transactionLocks);
         try (Connection connection = getConnection()) {
             makeReadOnly(connection);
+            if (selection != null) {
+                final String filter = selection.query(connection, null);
+                if (filter != null) {
+                    sql.append(" WHERE ").append(filter);
+                }
+            }
             try (Statement st = connection.createStatement();
                  ResultSet rs = st.executeQuery(sql.toString()))
             {
@@ -337,7 +346,7 @@ final class FeatureStream extends DeferredStream<Feature> {
                     if (!rs.wasNull()) return n;
                 }
             }
-        } catch (SQLException e) {
+        } catch (Exception e) {
             throw cannotExecute(e);
         } finally {
             unlock();
@@ -394,15 +403,13 @@ final class FeatureStream extends DeferredStream<Feature> 
{
      */
     @Override
     protected Spliterator<Feature> 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);
         final Connection connection = getConnection();
         setCloseHandler(connection);  // Executed only if `FeatureIterator` 
creation fails, discarded later otherwise.
         makeReadOnly(connection);
-        final var features = new FeatureIterator(table, connection, distinct, 
filter, sort, offset, count);
+        final var features = new FeatureIterator(table, connection, distinct, 
selection, sort, offset, count);
         setCloseHandler(features);
+        selection = null;             // Let the garbage collector do its work.
         return features;
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
index f8590e8d43..34d4a25d5c 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
@@ -489,7 +489,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
     private void log(final String method, final LogRecord warning) {
         if (warning != null) {
             warning.setLoggerName(Modules.SQL);
-            warning.setSourceClassName(getClass().getName());
+            warning.setSourceClassName(getClass().getCanonicalName());
             warning.setSourceMethodName(method);
             database.listeners.warning(warning);
         }
@@ -522,7 +522,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * responsible for holding a lock. It may be a read lock or write lock 
depending
      * on the {@link Connection#isReadOnly()} value.
      *
-     * @param  crs     the CRS for which to find a SRID, or {@code null}.
+     * @param  crs  the CRS for which to find a SRID, or {@code null}.
      * @return SRID for the given CRS, or 0 if the given CRS was null.
      * @throws Exception if an SQL error, parsing error or other error 
occurred.
      */
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
index 534146c158..7ea58675af 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
@@ -93,6 +93,11 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short IllegalQualifiedName_1 = 3;
 
+        /**
+         * The literal of function “{0}” is not compatible with the reference 
system of property “{1}”.
+         */
+        public static final short IncompatibleLiteralCRS_2 = 18;
+
         /**
          * Unexpected error while analyzing the database schema.
          */
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
index 77620d600b..1060cb1e1c 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
@@ -27,6 +27,7 @@ DataSource                        = Provider of connections 
to the database.
 DuplicatedColumn_1                = Unexpected duplication of column named 
\u201c{0}\u201d.
 DuplicatedSRID_2                  = Spatial Reference Identifier (SRID) {1} 
has more than one entry in \u201c{0}\u201d table.
 IllegalQualifiedName_1            = \u201c{0}\u201d is not a valid qualified 
name for a table.
+IncompatibleLiteralCRS_2          = The literal of function \u201c{0}\u201d is 
not compatible with the reference system of property \u201c{1}\u201d.
 InternalError                     = Unexpected error while analyzing the 
database schema.
 MalformedForeignerKey_2           = Unexpected column \u201c{1}\u201d in the 
\u201c{0}\u201d foreigner key.
 MappedSQLQueries                  = Resource names mapped to SQL queries.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
index 437f789ad5..3d6a4a87f0 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
@@ -32,6 +32,7 @@ DataSource                        = Fournisseur de connexions 
\u00e0 la base de
 DuplicatedColumn_1                = Doublon inattendu d\u2019une colonne 
nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb.
 DuplicatedSRID_2                  = L\u2019identifiant de r\u00e9f\u00e9rence 
spatiale (SRID) {1} a plusieurs entr\u00e9s dans la table 
\u00ab\u202f{0}\u202f\u00bb.
 IllegalQualifiedName_1            = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas 
un nom qualifi\u00e9 de table valide.
+IncompatibleLiteralCRS_2          = Le litt\u00e9ral de la fonction 
\u00ab\u202f{0}\u202f\u00bb n'est pas compatible avec le syst\u00e8me de 
r\u00e9f\u00e9rence de la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb.
 InternalError                     = Erreur inattendue pendant l\u2019analyse 
du sch\u00e9ma de la base de donn\u00e9es.
 MalformedForeignerKey_2           = Colonne \u00ab\u202f{1}\u202f\u00bb 
inattendue dans la cl\u00e9 \u00e9trang\u00e8re \u00ab\u202f{0}\u202f\u00bb.
 MappedSQLQueries                  = Noms de ressources associ\u00e9s \u00e0 
des requ\u00eates SQL.
diff --git 
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
index 819d204b07..aa0d7a92ab 100644
--- 
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,14 +16,29 @@
  */
 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;
@@ -37,20 +52,68 @@ import org.opengis.filter.ValueReference;
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-public final class SelectionClause extends SQLBuilder {
+public final class SelectionClause extends SQLBuilder implements 
Consumer<WarningEvent> {
+    /**
+     * Whether the database rejects spatial functions that mix geometries with 
and without <abbr>CRS</abbr>.
+     * We observed that PostGIS 3.4 produces an error not only when the 
geometry operands have different CRS,
+     * but also when one operand has a CRS and the other operand has no 
explicit CRS. Example:
+     *
+     * <blockquote>Operation on mixed SRID geometries (Polygon, 4326) != 
(Polygon, 0)</blockquote>
+     *
+     * As a workaround, if the literal has no CRS, we assume that the CRS of 
the geometry column was implied.
+     * In such cases, the SRID 0 is replaced by the SRID of the geometry 
column. Current version applies this
+     * workaround for all databases, but we could make this field non-static 
if a future version of Apache SIS
+     * decides to apply this policy on a case-by-case basis.
+     *
+     * <p>Note that above error is not always visible. For example, when using 
{@code ST_Intersects},
+     * PostGIS first performs a quick filtering based on bounding boxes using 
the {@code &&} operator.
+     * But that operator does not check the <abbr>SRID</abbr>. Therefore, no 
error message is raised
+     * when the bounding boxes of the geometries do not intersect.</p>
+     */
+    @Workaround(library = "PostGIS", version = "3.4")
+    static final boolean REPLACE_UNSPECIFIED_CRS = true;
+
     /**
      * The table or view for which to create a SQL statement.
      */
     private final Table table;
 
+    /**
+     * The parameters to set after the connection has been established.
+     * For each entry, the key is the index in {@link #buffer} of the 
parameter to set
+     * and the value is the object to convert to a value that can be set in 
the query.
+     * The following values need to be converted:
+     *
+     * <ul>
+     *   <li>Instances of {@link CoordinateReferenceSystem} shall be replaced 
by <abbr>SRID</abbr>.</li>
+     * </ul>
+     *
+     * Elements must be sorted in increasing order of keys.
+     *
+     * @see #query(InfoStatements)
+     */
+    private final List<Map.Entry<Integer, CoordinateReferenceSystem>> 
parameters;
+
+    /**
+     * The coordinate reference system of the geometry columns referenced by 
the spatial functions to write.
+     * This is {@code null} when not writing a spatial function, or if the 
function does not reference any
+     * geometry column, or if it references more than one geometry column with 
different <abbr>CRS</abbr>.
+     * If non-null, the optional may be empty if the geometry column does not 
declare a reference system.
+     * In the latter case (empty), each row of the table can be a geometry in 
a different <abbr>CRS</abbr>.
+     *
+     * @see #REPLACE_UNSPECIFIED_CRS
+     */
+    private Optional<CoordinateReferenceSystem> columnCRS;
+
     /**
      * Flag sets to {@code true} if a filter or expression cannot be converted 
to SQL.
      * When a SQL string become flagged as invalid, it is truncated to the 
length that
      * it had the last time that it was valid.
      *
      * @see #invalidate()
+     * @see #isInvalid()
      */
-    boolean isInvalid;
+    private boolean isInvalid;
 
     /**
      * Creates a new builder for the given table.
@@ -60,6 +123,39 @@ public final class SelectionClause extends SQLBuilder {
     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 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;
     }
 
     /**
@@ -84,10 +180,10 @@ public final class SelectionClause extends SQLBuilder {
             appendGeometry(null, new GeneralEnvelope((GeographicBoundingBox) 
value));
         } else if (value instanceof Envelope) {
             appendGeometry(null, (Envelope) value);
-        } else if (value instanceof Geometry) {
-            appendGeometry(Geometries.wrap((Geometry) value).orElse(null), 
null);
         } else {
-            appendValue(value);
+            Geometries.wrap(value).ifPresentOrElse(
+                    (wrapper) -> appendGeometry(wrapper, null),
+                    ()        -> appendValue(value));
         }
     }
 
@@ -111,9 +207,14 @@ public final class SelectionClause extends SQLBuilder {
             invalidate();
             return;
         }
+        /*
+         * Get the average span of the geometry. It will be used for computing 
a flatness factor.
+         * It does not matter if the span is inaccurate, as the flatness 
factor is only a hint
+         * ignored by most geometry libraries.
+         */
         final double span = (bounds.getSpan(0) + bounds.getSpan(1)) / 
Geometries.BIDIMENSIONAL;
         if (Double.isNaN(span)) {
-            final GeneralEnvelope e = new GeneralEnvelope(bounds);
+            final var e = new GeneralEnvelope(bounds);
             for (int i=0; i<Geometries.BIDIMENSIONAL; i++) {
                 final double lower = clampInfinity(e.getLower(i));
                 final double upper = clampInfinity(e.getUpper(i));
@@ -128,8 +229,40 @@ public final class SelectionClause extends SQLBuilder {
         if (wrapper == null) {
             wrapper = table.database.geomLibrary.toGeometry2D(bounds, 
WraparoundMethod.SPLIT);
         }
-        final String wkt = wrapper.formatWKT(0.05 * span);
-        append("ST_GeomFromText(").appendValue(wkt).append(')');
+        final String wkt = wrapper.formatWKT(0.05 * span);      // Arbitrary 
flateness factor.
+        /*
+         * Format a spatial function for building the geometry from the 
Well-Known Text.
+         * The CRS, if available, while be specified as a SRID if the spatial 
support has
+         * been recognized (otherwise we cannot map the CRS to the 
database-dependent SRID).
+         */
+        appendSpatialFunction("ST_GeomFromText");
+        append('(').appendValue(wkt);
+        if (table.database.getSpatialSchema().isPresent()) {
+            CoordinateReferenceSystem crs = 
wrapper.getCoordinateReferenceSystem();
+            if (REPLACE_UNSPECIFIED_CRS && columnCRS != null) {
+                if (crs == null) {
+                    crs = columnCRS.orElse(null);
+                } else {
+                    /*
+                     * If `columnCRS` is empty, then we have a geometry column 
without CRS (SRID = 0).
+                     * For making the literal consistent with the column, we 
could set `crs` to null.
+                     * However, while the column has no CRS, the geometry 
instances on each row may have a CRS
+                     * and clearing the literal CRS will result in "Operation 
on mixed SRID geometries" error.
+                     *
+                     * The opposite problem also exists: we could really have 
no CRS at all, neither in the column
+                     * and in the geometry instances. In such case, not 
clearing the CRS may also cause above error.
+                     * We have no easy way to determine if we should clear the 
CRS or not. The conservative approach
+                     * applied for now is to leave the literal as the user 
specified it.
+                     */
+                }
+            }
+            if (crs != null) {
+                buffer.append(", ");
+                parameters.add(new AbstractMap.SimpleEntry<>(buffer.length(), 
crs));
+                buffer.append('?');
+            }
+        }
+        append(')');
     }
 
     /**
@@ -142,11 +275,16 @@ public final class SelectionClause extends SQLBuilder {
     }
 
     /**
-     * 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.
+     * Appends the name of a spatial function. The catalog and schema names are
+     * included for making sure that it works even if the search path is not 
set.
+     * The function name is written without quotes, because the functions kept 
by
+     * {@link SelectionClauseWriter#removeUnsupportedFunctions(Database)} use 
the
+     * case convention of the database.
+     *
+     * @param  name  name of the spatial function to append.
      */
-    final void invalidate() {
-        isInvalid = true;
+    final void appendSpatialFunction(final String name) {
+        appendIdentifier(table.database.catalogOfSpatialTables, 
table.database.schemaOfSpatialTables, name, false);
     }
 
     /**
@@ -170,4 +308,90 @@ public final class SelectionClause extends SQLBuilder {
         }
         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().flatMap(CodeList::identifier).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 --git 
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
index 208fb33c89..a9e96dd22c 100644
--- 
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
@@ -231,7 +231,7 @@ public class SelectionClauseWriter extends Visitor<Feature, 
SelectionClause> {
     @SuppressWarnings("unchecked")
     final boolean write(final SelectionClause sql, final Filter<? super 
Feature> filter) {
         visit((Filter<Feature>) filter, sql);
-        return sql.isInvalid;
+        return sql.isInvalid();
     }
 
     /**
@@ -243,7 +243,7 @@ public class SelectionClauseWriter extends Visitor<Feature, 
SelectionClause> {
      */
     private boolean write(final SelectionClause sql, final Expression<Feature, 
?> expression) {
         visit(expression, sql);
-        return sql.isInvalid;
+        return sql.isInvalid();
     }
 
     /**
@@ -387,7 +387,8 @@ public class SelectionClauseWriter extends Visitor<Feature, 
SelectionClause> {
     /**
      * 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> {
         /** Name the function. */
@@ -398,10 +399,27 @@ public class SelectionClauseWriter extends 
Visitor<Feature, SelectionClause> {
             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) {
-            sql.append(name);
-            writeParameters(sql, filter.getExpressions(), ", ", false);
+            sql.appendSpatialFunction(name);
+            final List<Expression<Feature, ?>> expressions = 
filter.getExpressions();
+            if (SelectionClause.REPLACE_UNSPECIFIED_CRS) {
+                for (Expression<Feature,?> exp : expressions) {
+                    if (exp instanceof ValueReference<?,?>) {
+                        if (sql.acceptColumnCRS((ValueReference<Feature,?>) 
exp)) {
+                            break;
+                        }
+                    }
+                }
+            }
+            writeParameters(sql, expressions, ", ", false);
+            sql.clearColumnCRS();
         }
     }
 }
diff --git 
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
index 932e3fde96..13856b4c89 100644
--- 
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
@@ -421,7 +421,7 @@ final class Table extends AbstractFeatureSet {
         if (query != null) {
             sql.append('(').append(query).append(") AS USER_QUERY");
         } else {
-            sql.appendIdentifier(name.catalog, name.schema, name.table);
+            sql.appendIdentifier(name.catalog, name.schema, name.table, true);
         }
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedClauseWriter.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedClauseWriter.java
index f2e40cd0b2..0e0ee0504b 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedClauseWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedClauseWriter.java
@@ -26,6 +26,11 @@ import org.opengis.filter.SpatialOperatorName;
  * Converter from filters/expressions to the {@code WHERE} part of SQL 
statement
  * with PostGIS-specific syntax where useful.
  *
+ * This class adds the search by bounding box using the {@code &&} operator.
+ * Note that contrarily to standard operators such as {@code ST_Intersects},
+ * the {@code &&} operator does not verify the <abbr>CRS</abbr>.
+ * No error message is raised is case of mismatched CRS.
+ *
  * @author  Alexis Manin (Geomatys)
  */
 final class ExtendedClauseWriter extends SelectionClauseWriter {
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java
index 5865e933c1..d11dd644fb 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java
@@ -98,7 +98,7 @@ final class ExtentEstimator {
     GeneralEnvelope estimate(final Statement statement, final boolean recall) 
throws SQLException {
         query(statement);
         if (envelope == null && !recall) {
-            builder.append("ANALYZE ").appendIdentifier(table.catalog, 
table.schema, table.table);
+            builder.append("ANALYZE ").appendIdentifier(table.catalog, 
table.schema, table.table, true);
             final String sql = builder.toString();
             builder.clear();
             statement.execute(sql);
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java
 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java
index 3158ada9a8..9e13d21269 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java
@@ -162,6 +162,7 @@ public final class GeometryGetterTest extends TestCase {
     public static CoordinateReferenceSystem getExpectedCRS(final int srid) 
throws FactoryException {
         final String code;
         switch (srid) {
+            case 0:    return null;
             case 3395: code = "EPSG:3395"; break;
             case 4326: return CommonCRS.WGS84.normalizedGeographic();
             default:   throw new AssertionError(srid);
diff --git 
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
index 2babdb9146..f9ed98cc6b 100644
--- 
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
@@ -30,6 +30,8 @@ import java.nio.channels.ReadableByteChannel;
 import java.lang.reflect.Method;
 import org.locationtech.jts.geom.Point;
 import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Coordinate;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.identification.Identification;
@@ -46,7 +48,9 @@ import org.apache.sis.storage.sql.SQLStoreProvider;
 import org.apache.sis.storage.sql.SimpleFeatureStore;
 import org.apache.sis.storage.sql.ResourceDefinition;
 import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.io.stream.ChannelDataInput;
+import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.sql.feature.BinaryEncoding;
 import org.apache.sis.geometry.wrapper.jts.JTS;
 import org.apache.sis.referencing.CommonCRS;
@@ -129,8 +133,8 @@ public final class PostgresTest extends TestCase {
             database.executeSQL(List.of(resource("SpatialFeatures.sql")));
             final var connector = new StorageConnector(database.source);
             connector.setOption(OptionKey.GEOMETRY_LIBRARY, 
GeometryLibrary.JTS);
-            final ResourceDefinition table = ResourceDefinition.table(null, 
SQLStoreTest.SCHEMA, "SpatialData");
-            try (SimpleFeatureStore store = new SimpleFeatureStore(new 
SQLStoreProvider(), connector, table)) {
+            final var table = ResourceDefinition.table(null, 
SQLStoreTest.SCHEMA, "SpatialData");
+            try (var store = new SimpleFeatureStore(new SQLStoreProvider(), 
connector, table)) {
                 validate(store.getMetadata());
                 /*
                  * Invoke the private `model()` method. We have to use 
reflection because the class
@@ -151,12 +155,9 @@ public final class PostgresTest extends TestCase {
                  * Tests through public API.
                  */
                 final FeatureSet resource = store.findResource("SpatialData");
-                try (Stream<Feature> features = resource.features(false)) {
-                    features.forEach(PostgresTest::validate);
-                }
-                final Envelope envelope = resource.getEnvelope().get();
-                assertEquals(envelope.getMinimum(0), -72, 1);
-                assertEquals(envelope.getMaximum(1),  43, 1);
+                testAllFeatures(resource);
+                testFilteredFeatures(resource, false);
+                testFilteredFeatures(resource, true);
             }
         }
     }
@@ -183,7 +184,7 @@ public final class PostgresTest extends TestCase {
      * @throws Exception if an error occurred while testing the database.
      */
     private static void testGeometryGetter(final ExtendedInfo info, final 
Connection connection) throws Exception {
-        final GeometryGetterTest test = new GeometryGetterTest();
+        final var test = new GeometryGetterTest();
         test.testFromDatabase(connection, info, BinaryEncoding.HEXADECIMAL);
     }
 
@@ -193,8 +194,8 @@ public final class PostgresTest extends TestCase {
     private static void testRasterReader(final TestRaster test, final 
ExtendedInfo info, final Connection connection)
             throws Exception
     {
-        final BinaryEncoding encoding = BinaryEncoding.HEXADECIMAL;
-        final RasterReader reader = new RasterReader(info);
+        final var encoding = BinaryEncoding.HEXADECIMAL;
+        final var reader = new RasterReader(info);
         try (PreparedStatement stmt = connection.prepareStatement("SELECT 
image FROM features.\"SpatialData\" WHERE filename=?")) {
             stmt.setString(1, test.filename);
             final ResultSet r = stmt.executeQuery();
@@ -206,6 +207,62 @@ public final class PostgresTest extends TestCase {
         }
     }
 
+    /**
+     * 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)) {
+            features.forEach(PostgresTest::validate);
+        }
+        try (Stream<Feature> 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)) {
+            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.
@@ -222,8 +279,15 @@ public final class PostgresTest extends TestCase {
                 assertSame(CommonCRS.WGS84.normalizedGeographic(), 
raster.getCoordinateReferenceSystem());
                 return;
             }
+            case "point-nocrs": {
+                var p = (Point) geometry;
+                assertEquals(3, p.getX());
+                assertEquals(4, p.getY());
+                geomSRID = 0;
+                break;
+            }
             case "point-prj": {
-                final Point p = (Point) geometry;
+                var p = (Point) geometry;
                 assertEquals(2, p.getX());
                 assertEquals(3, p.getY());
                 geomSRID = 3395;
@@ -239,9 +303,13 @@ public final class PostgresTest extends TestCase {
         try {
             final CoordinateReferenceSystem expected = 
GeometryGetterTest.getExpectedCRS(geomSRID);
             final CoordinateReferenceSystem actual = 
JTS.getCoordinateReferenceSystem(geometry);
-            assertNotNull(actual);
-            if (expected != null) {
-                assertEquals(expected, actual);
+            if (geomSRID == 0) {
+                assertNull(actual);
+            } else {
+                assertNotNull(actual);
+                if (expected != null) {
+                    assertEquals(expected, actual);
+                }
             }
         } catch (FactoryException e) {
             throw new AssertionError(e);
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/SpatialFeatures.sql
 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/SpatialFeatures.sql
index 2dec522994..f6853ae912 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/SpatialFeatures.sql
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/SpatialFeatures.sql
@@ -11,6 +11,7 @@ SET search_path TO public;
 CREATE TABLE features."SpatialData" (
     "filename" VARCHAR(20) NOT NULL,
     "geometry" GEOMETRY,
+    "geom4326" GEOMETRY(Geometry, 4326),
     "image"    RASTER,
 
     CONSTRAINT "PK_SpatialData" PRIMARY KEY ("filename")
@@ -32,6 +33,7 @@ INSERT INTO features."SpatialData" ("filename", "image") 
VALUES
 -- Geometries with arbitrary coordinate values.
 --
 INSERT INTO features."SpatialData" ("filename", "geometry") VALUES
+  ('point-nocrs', ST_GeomFromText('POINT(3 4)')),
   ('point-prj',   ST_GeomFromText('POINT(2 3)', 3395)),
   ('linestring',  ST_GeomFromText('LINESTRING(-71.160281 42.258729,-71.160837 
42.259113,-71.161144 42.25932)', 4326)),
   ('polygon-prj', ST_GeomFromText('POLYGON((0 0,0 1,1 1,1 0,0 0))', 3395)),
@@ -60,6 +62,12 @@ INSERT INTO features."SpatialData" ("filename", "geometry") 
VALUES
    ||   '-71.1043850704575 42.3150793250568,-71.1043632495873 
42.315113108546)))', 4326));
 
 
+--
+-- A column where all geometries are in the same CRS.
+--
+UPDATE features."SpatialData" SET geom4326 = geometry WHERE filename NOT LIKE 
'%-prj';
+
+
 --
 -- Geometries with WKT representation in one column and WKB in another column.
 -- Used for parsing the same geometry in two ways and comparing the results.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreFormat.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreFormat.java
index 4262b5461d..fc3a29b6d7 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreFormat.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreFormat.java
@@ -171,7 +171,7 @@ public final class StoreFormat extends WKTFormat {
     private void log(final Exception e) {
         final LogRecord record = Resources.forLocale(listeners.getLocale())
                 .getLogRecord(Level.WARNING, 
Resources.Keys.CanNotReadCRS_WKT_1, listeners.getSourceName());
-        record.setSourceClassName(listeners.getSource().getClass().getName());
+        
record.setSourceClassName(listeners.getSource().getClass().getCanonicalName());
         record.setSourceMethodName("getMetadata");
         record.setLoggerName(Loggers.WKT);
         listeners.warning(record);
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/GeometryLibrary.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/GeometryLibrary.java
index d11db13b12..acc07cbf18 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/GeometryLibrary.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/GeometryLibrary.java
@@ -28,7 +28,7 @@ import org.opengis.metadata.acquisition.GeometryType;
  * All those libraries are optional.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see OptionKey#GEOMETRY_LIBRARY
  * @see 
org.apache.sis.feature.builder.FeatureTypeBuilder#addAttribute(GeometryType)
@@ -52,7 +52,7 @@ public enum GeometryLibrary {
      * Note that contrarily to JTS and ESRI libraries,
      * a point does not extend any root geometry class in Java2D.
      */
-    JAVA2D,
+    JAVA2D("Java2D"),
 
     /**
      * The ESRI geometry API library. This library can be used for spatial 
vector data processing.
@@ -70,7 +70,7 @@ public enum GeometryLibrary {
      *
      * @see <a href="https://github.com/Esri/geometry-api-java/wiki";>API wiki 
page</a>
      */
-    ESRI,
+    ESRI("ESRI"),
 
     /**
      * The Java Topology Suite (JTS) library. This open source library 
provides an object model
@@ -90,7 +90,7 @@ public enum GeometryLibrary {
      *
      * @since 1.0
      */
-    JTS,
+    JTS("JTS"),
 
     /**
      * The GeoAPI geometry interfaces.
@@ -111,5 +111,32 @@ public enum GeometryLibrary {
      *
      * @since 1.4
      */
-    GEOAPI
+    GEOAPI("GeoAPI");
+
+    /**
+     * Human-readable name of this library.
+     */
+    private final String name;
+
+    /**
+     * Creates a new enumeration value.
+     *
+     * @param  name  human-readable name of this library.
+     */
+    private GeometryLibrary(final String name) {
+        this.name = name;
+    }
+
+    /**
+     * Returns the name of this geometry library in a way suitable to user 
interfaces.
+     * This is the same as {@link #name()} but sometime with different cases.
+     * For example, {@link #JAVA2D} is shown as {@code "Java2D"}.
+     *
+     * @return human-readable name of this library.
+     *
+     * @since 1.5
+     */
+    public String toString() {
+        return name;
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java
index 1e2d88e2a1..cf021c4af3 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java
@@ -173,7 +173,7 @@ abstract class WeakEntry<E> extends WeakReference<E> 
implements Disposable {
             final LogRecord record = 
Messages.forLocale(null).getLogRecord(Level.FINEST,
                     Messages.Keys.ChangedContainerCapacity_2, oldTable.length, 
table.length);
             record.setSourceMethodName(callerMethod);
-            record.setSourceClassName(entryType.getEnclosingClass().getName());
+            
record.setSourceClassName(entryType.getEnclosingClass().getCanonicalName());
             record.setLoggerName(LOGGER.getName());
             LOGGER.log(record);
         }
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java
index 530ceb6218..4a917c00f1 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java
@@ -318,7 +318,7 @@ public class GeoHeifStore extends DataStore implements 
Aggregate {
      * Logs a warning emitted (usually indirectly) by {@link #components()}.
      */
     final void warning(final LogRecord record) {
-        record.setSourceClassName(GeoHeifStore.class.getSimpleName());
+        record.setSourceClassName(GeoHeifStore.class.getCanonicalName());
         record.setSourceMethodName("components");
         listeners.warning(record);
     }

Reply via email to