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

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

commit aa29a64835f97d51b4d35750d6e2f18e77a1effe
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Nov 5 17:22:56 2025 +0100

    Add support for `IS_NAN`, `IS_FINITE` and `IS_INFINITE` expressions.
    Continuation of https://issues.apache.org/jira/browse/SIS-622
---
 .../org.apache.sis.feature/main/module-info.java   |   3 +
 .../main/org/apache/sis/filter/Capabilities.java   |  21 ++-
 .../main/org/apache/sis/filter/math/Function.java  |  81 ++++++--
 .../math/{UnaryOperator.java => Predicate.java}    |  32 ++--
 .../main/org/apache/sis/filter/math/Registry.java  |  15 +-
 .../org/apache/sis/filter/math/UnaryOperator.java  |   4 +-
 .../main/org/apache/sis/filter/sqlmm/SQLMM.java    |   7 +-
 .../sis/filter/visitor/FunctionIdentifier.java     |  56 ++++++
 .../apache/sis/filter/visitor/FunctionNames.java   |   4 +-
 .../org/apache/sis/filter/visitor/Visitor.java     |  52 ++++--
 .../metadata/sql/internal/shared/Reflection.java   |  22 +++
 .../metadata/sql/internal/shared/TypeMapper.java   |   3 +-
 .../storage/sql/duckdb/ExtendedClauseWriter.java   |  16 +-
 .../storage/sql/feature/SelectionClauseWriter.java | 203 ++++++++++++++++-----
 .../storage/sql/postgis/ExtendedClauseWriter.java  |  16 +-
 .../org/apache/sis/storage/sql/SQLStoreTest.java   |  33 +++-
 .../sql/feature/SelectionClauseWriterTest.java     |   8 +-
 17 files changed, 432 insertions(+), 144 deletions(-)

diff --git a/endorsed/src/org.apache.sis.feature/main/module-info.java 
b/endorsed/src/org.apache.sis.feature/main/module-info.java
index 4e4f4273b6..422dfde078 100644
--- a/endorsed/src/org.apache.sis.feature/main/module-info.java
+++ b/endorsed/src/org.apache.sis.feature/main/module-info.java
@@ -56,6 +56,9 @@ module org.apache.sis.feature {
             org.apache.sis.storage.shapefile,       // In the "incubator" 
sub-project.
             org.apache.sis.cql;                     // In the "incubator" 
sub-project.
 
+    exports org.apache.sis.filter.math to
+            org.apache.sis.storage.sql;
+
     exports org.apache.sis.filter.sqlmm to
             org.apache.sis.geometry;                // In the "incubator" 
sub-project.
 
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Capabilities.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Capabilities.java
index f226faa3a1..edf21afb88 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Capabilities.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Capabilities.java
@@ -49,7 +49,7 @@ import org.opengis.filter.capability.TemporalCapabilities;
 
 /**
  * Metadata about the specific elements that Apache <abbr>SIS</abbr> 
implementation supports.
- * This is also an unmodifiable map of all functions supported by this factory,
+ * This is also an unmodifiable map of functions supported by this factory,
  * together with their providers. This class is thread-safe.
  *
  * @todo Missing {@link SpatialCapabilities} and {@link TemporalCapabilities}.
@@ -62,7 +62,8 @@ final class Capabilities extends AbstractMap<String, 
AvailableFunction>
         implements FilterCapabilities, Conformance, IdCapabilities, 
ScalarCapabilities
 {
     /**
-     * The providers of functions. Each {@code FunctionRegister} instance 
typically provides many functions.
+     * The providers of functions.
+     * Each {@code FunctionRegister} instance typically provides many 
functions.
      * There is one provider for SQL/MM, one provider for mathematical 
functions, <i>etc</i>.
      */
     private final LazySet<FunctionRegister> providers;
@@ -168,7 +169,7 @@ final class Capabilities extends AbstractMap<String, 
AvailableFunction>
     }
 
     /**
-     * Indicates that SIS supports <i>And</i>, <i>Or</i> and <i>Not</i> 
operators.
+     * Advertises that Apache <abbr>SIS</abbr> supports <i>And</i>, <i>Or</i> 
and <i>Not</i> operators.
      */
     @Override
     public boolean hasLogicalOperators() {
@@ -184,7 +185,7 @@ final class Capabilities extends AbstractMap<String, 
AvailableFunction>
     }
 
     /**
-     * Advertises that SIS supports all comparison operators.
+     * Advertises that <abbr>SIS</abbr> supports all comparison operators.
      */
     @Override
     public Set<ComparisonOperatorName> getComparisonOperators() {
@@ -192,7 +193,7 @@ final class Capabilities extends AbstractMap<String, 
AvailableFunction>
     }
 
     /**
-     * Indicates that Apache SIS supports the <i>Spatial Filter</i> 
conformance level.
+     * Advertises that Apache <abbr>SIS</abbr> supports the <i>Spatial 
Filter</i> conformance level.
      *
      * @todo Need to implement {@linkplain 
FilterCapabilities#getSpatialCapabilities() temporal capabilities}.
      */
@@ -202,7 +203,7 @@ final class Capabilities extends AbstractMap<String, 
AvailableFunction>
     }
 
     /**
-     * Indicates that Apache SIS supports the <i>Temporal Filter</i> 
conformance level.
+     * Advertises that Apache <abbr>SIS</abbr> supports the <i>Temporal 
Filter</i> conformance level.
      *
      * @todo Need to implement {@linkplain 
FilterCapabilities#getTemporalCapabilities() temporal capabilities}.
      */
@@ -212,7 +213,7 @@ final class Capabilities extends AbstractMap<String, 
AvailableFunction>
     }
 
     /**
-     * Indicates that Apache SIS supports the <i>Sorting</i> conformance level.
+     * Advertises that Apache <abbr>SIS</abbr> supports the <i>Sorting</i> 
conformance level.
      */
     @Override
     public boolean implementsSorting() {
@@ -222,7 +223,7 @@ final class Capabilities extends AbstractMap<String, 
AvailableFunction>
     /**
      * Enumerates the functions that may be used in filter expressions.
      *
-     * @return the function that may be used in filter expressions.
+     * @return the functions that may be used in filter expressions.
      */
     @Override
     public Map<String, AvailableFunction> getFunctions() {
@@ -271,8 +272,8 @@ final class Capabilities extends AbstractMap<String, 
AvailableFunction>
     /**
      * Returns the index of the provider for the function of the given name.
      * This method shall be invoked in a block synchronized on {@code this}.
-     * If many providers define a function of the given name, then there is
-     * no guaranteed about which provider is returned.
+     * If many providers define a function of the given name,
+     * then there is no guaranteed about which provider is returned.
      *
      * @param  name  case-insensitive name of the function to get.
      * @return index of the provider for the given name, or -1 if not found.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Function.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Function.java
index 95d2b36864..d96bfab94c 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Function.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Function.java
@@ -18,14 +18,18 @@ package org.apache.sis.filter.math;
 
 import java.util.Map;
 import java.util.List;
+import java.util.Arrays;
 import java.util.function.DoubleUnaryOperator;
 import java.util.function.DoubleBinaryOperator;
+import java.util.function.DoublePredicate;
+import java.sql.Types;
 import org.opengis.util.TypeName;
 import org.opengis.util.LocalName;
 import org.opengis.util.ScopedName;
 import org.opengis.parameter.ParameterDescriptor;
 import org.apache.sis.parameter.DefaultParameterDescriptor;
 import org.apache.sis.feature.internal.shared.FeatureExpression;
+import org.apache.sis.filter.visitor.FunctionIdentifier;
 import org.apache.sis.filter.base.Node;
 import org.apache.sis.util.iso.Names;
 
@@ -42,7 +46,7 @@ import org.opengis.feature.AttributeType;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-enum Function implements AvailableFunction {
+public enum Function implements FunctionIdentifier, AvailableFunction {
     /*
      * MIN and MAX are omitted because it needs more generic code working with 
Comparable.
      * We may need specializations here for the handling of NaN, but this is 
deferred to a
@@ -93,7 +97,7 @@ enum Function implements AvailableFunction {
     /**
      * The value of <var>x</var> raised to the power of <var>y</var>.
      */
-    POWER(Math::pow),
+    POWER(null, null, Math::pow),
 
     /**
      * The square-root value of <var>x</var>.
@@ -108,7 +112,7 @@ enum Function implements AvailableFunction {
     /**
      * Hypotenuse of <var>x</var> and <var>y</var>.
      */
-    HYPOT(Math::hypot),
+    HYPOT(null, null, Math::hypot),
 
     /**
      * The arc sine of <var>x</var>.
@@ -129,7 +133,7 @@ enum Function implements AvailableFunction {
      * The arc tangent of <var>y</var>/<var>x</var>.
      * Note that <var>y</var> is the first argument and <var>x</var> is the 
second argument.
      */
-    ATAN2(Math::atan2),
+    ATAN2(null, null, Math::atan2),
 
     /**
      * The hyperbolic sine of <var>x</var>.
@@ -144,18 +148,38 @@ enum Function implements AvailableFunction {
     /**
      * The hyperbolic tangent of <var>x</var>.
      */
-    TANH(Math::tanh);
+    TANH(Math::tanh),
+
+    /**
+     * Returns whether the specified number is neither infinite of NaN.
+     */
+    IS_FINITE(Double::isFinite, null, null),
+
+    /**
+     * Returns whether the specified number is positive or negative infinity.
+     */
+    IS_INFINITE(Double::isInfinite, null, null),
+
+    /**
+     * Returns whether the specified number is a Not-a-Number (NaN) value.
+     */
+    IS_NAN(Double::isNaN, null, null);
+
+    /**
+     * The mathematical function to invoke if this operation is a predicate, 
or {@code null}.
+     */
+    final DoublePredicate filter;
 
     /**
      * The mathematical function to invoke if this operation is unary, or 
{@code null}.
      */
-    @SuppressWarnings("serial")
     final DoubleUnaryOperator unary;
 
     /**
      * The mathematical function to invoke if this operation is binary, or 
{@code null}.
+     * Usually, only one of the {@link #unary} and {@code binary} fields is 
non-null.
+     * But we may allow both of them to be non-null if the function if 
overloaded.
      */
-    @SuppressWarnings("serial")
     final DoubleBinaryOperator binary;
 
     /**
@@ -170,29 +194,31 @@ enum Function implements AvailableFunction {
      *
      * @see #getResultType()
      */
-    private AttributeType<Double> resultType;
+    private AttributeType<?> resultType;
 
     /**
      * Creates a new function description for a unary operation.
      */
     private Function(final DoubleUnaryOperator math) {
+        filter = null;
         unary  = math;
         binary = null;
     }
 
     /**
-     * Creates a new function description for a binary operation.
+     * Creates a new function description.
      */
-    private Function(final DoubleBinaryOperator math) {
-        unary  = null;
-        binary = math;
+    private Function(final DoublePredicate filter, final DoubleUnaryOperator 
unary, final DoubleBinaryOperator binary) {
+        this.filter = filter;
+        this.unary  = unary;
+        this.binary = binary;
     }
 
     /**
      * Returns the minimum number of parameters expected by this function.
      */
     final int getMinParameterCount() {
-        if (unary  != null) return 1;
+        if (unary  != null || filter != null) return 1;
         if (binary != null) return 2;
         return 0;
     }
@@ -202,7 +228,7 @@ enum Function implements AvailableFunction {
      */
     final int getMaxParameterCount() {
         if (binary != null) return 2;
-        if (unary  != null) return 1;
+        if (unary  != null || filter != null) return 1;
         return 0;
     }
 
@@ -229,9 +255,13 @@ enum Function implements AvailableFunction {
     /**
      * Returns the attribute type to declare in feature types that store 
result of this function.
      */
-    final synchronized AttributeType<Double> getResultType() {
+    final synchronized AttributeType<?> getResultType() {
         if (resultType == null) {
-            resultType = Node.createType(Double.class, name());
+            if (filter == null) {
+                resultType = Node.createType(Double.class, name());
+            } else {
+                resultType = Node.createType(Boolean.class, name());
+            }
         }
         return resultType;
     }
@@ -276,4 +306,23 @@ enum Function implements AvailableFunction {
                 Map.of(DefaultParameterDescriptor.NAME_KEY, name),
                 1, 1, Number.class, null, null, null);
     }
+
+    /**
+     * Returns the types of arguments and the type of the return value of this 
function.
+     * The {@code dataTypes[0]} array element is the data type of the 
function's return value.
+     * All other array elements are the data types of the function's 
parameters, in order.
+     * The values of all array elements are constants of the {@link Types} 
class.
+     *
+     * @param  dataTypes  data type of the return value followed by 
parameters, as {@link java.sql.Types} constants.
+     * @return whether the specified return type and argument data types are 
valid for this function.
+     */
+    @Override
+    public int[] getSignature() {
+        final int[] dataTypes = new int[getMaxParameterCount() + 1];
+        Arrays.fill(dataTypes, Types.DOUBLE);
+        if (filter != null) {
+            dataTypes[0] = Types.BOOLEAN;
+        }
+        return dataTypes;
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/UnaryOperator.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Predicate.java
similarity index 74%
copy from 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/UnaryOperator.java
copy to 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Predicate.java
index e947ff89ac..5e0e31be44 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/UnaryOperator.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Predicate.java
@@ -17,7 +17,7 @@
 package org.apache.sis.filter.math;
 
 import java.util.Objects;
-import java.util.function.DoubleUnaryOperator;
+import java.util.function.DoublePredicate;
 import java.io.ObjectStreamException;
 import org.opengis.util.ScopedName;
 import org.apache.sis.filter.Optimization;
@@ -30,19 +30,19 @@ import org.opengis.filter.Expression;
 
 
 /**
- * An operation on a single operand.
+ * An operation on a single operand and returning a Boolean value.
  *
  * @author  Martin Desruisseaux (Geomatys)
  *
  * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
  */
-final class UnaryOperator<R> extends UnaryFunction<R, Number>
-        implements FeatureExpression<R, Double>, Optimization.OnExpression<R, 
Double>
+final class Predicate<R> extends UnaryFunction<R, Number>
+        implements FeatureExpression<R, Boolean>, Optimization.OnExpression<R, 
Boolean>
 {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -6215509464490587978L;
+    private static final long serialVersionUID = -5550022435116093162L;
 
     /**
      * The function to apply.
@@ -50,24 +50,24 @@ final class UnaryOperator<R> extends UnaryFunction<R, 
Number>
     private final Function function;
 
     /**
-     * The {@link Function#binary} value, guaranteed non-null.
+     * The {@link Function#filter} value, guaranteed non-null.
      */
-    private final transient DoubleUnaryOperator math;
+    private final transient DoublePredicate math;
 
     /**
-     * Creates a new function.
+     * Creates a new filter.
      */
-    UnaryOperator(final Function function, final Expression<R, ? extends 
Number> expression) {
+    Predicate(final Function function, final Expression<R, ? extends Number> 
expression) {
         super(expression);
         this.function = function;
-        math = Objects.requireNonNull(function.unary);
+        math = Objects.requireNonNull(function.filter);
     }
 
     /**
      * Invoked at deserialization time for setting the {@link #math} field.
      */
     private Object readResolve() throws ObjectStreamException {
-        return new UnaryOperator<>(function, expression);
+        return new Predicate<>(function, expression);
     }
 
     /**
@@ -82,8 +82,8 @@ final class UnaryOperator<R> extends UnaryFunction<R, Number>
      * Returns the type of values computed by this expression.
      */
     @Override
-    public final Class<Double> getResultClass() {
-        return Double.class;
+    public final Class<Boolean> getResultClass() {
+        return Boolean.class;
     }
 
     /**
@@ -99,10 +99,10 @@ final class UnaryOperator<R> extends UnaryFunction<R, 
Number>
      * Evaluates the expression.
      */
     @Override
-    public final Double apply(final R feature) {
-        final Number value  = expression.apply(feature);
+    public final Boolean apply(final R feature) {
+        final Number value = expression.apply(feature);
         if (value != null) {
-            return math.applyAsDouble(value.doubleValue());
+            return math.test(value.doubleValue());
         }
         return null;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Registry.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Registry.java
index d36b1ea8e8..94ad94e5aa 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Registry.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Registry.java
@@ -87,14 +87,15 @@ public final class Registry implements FunctionRegister {
                                           function.getMinParameterCount(),
                                           function.getMaxParameterCount(),
                                           parameters.length);
-        switch (parameters.length) {
-            case 1: return new UnaryOperator<>(function,
-                    parameters[0].toValueType(Number.class));
 
-            case 2: return new BinaryOperator<>(function,
-                    parameters[0].toValueType(Number.class),
-                    parameters[1].toValueType(Number.class));
+        final Expression<R, Number> p0 = 
parameters[0].toValueType(Number.class);
+        if (parameters.length == 1) {
+            if (function.filter != null) {
+                return new Predicate<>(function, p0);
+            } else {
+                return new UnaryOperator<>(function, p0);
+            }
         }
-        throw new IllegalArgumentException();   // Should never happen.
+        return new BinaryOperator<>(function, p0, 
parameters[1].toValueType(Number.class));
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/UnaryOperator.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/UnaryOperator.java
index e947ff89ac..535dfb33d2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/UnaryOperator.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/UnaryOperator.java
@@ -50,7 +50,7 @@ final class UnaryOperator<R> extends UnaryFunction<R, Number>
     private final Function function;
 
     /**
-     * The {@link Function#binary} value, guaranteed non-null.
+     * The {@link Function#unary} value, guaranteed non-null.
      */
     private final transient DoubleUnaryOperator math;
 
@@ -100,7 +100,7 @@ final class UnaryOperator<R> extends UnaryFunction<R, 
Number>
      */
     @Override
     public final Double apply(final R feature) {
-        final Number value  = expression.apply(feature);
+        final Number value = expression.apply(feature);
         if (value != null) {
             return math.applyAsDouble(value.doubleValue());
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SQLMM.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SQLMM.java
index 748d06f137..95ff407e76 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SQLMM.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SQLMM.java
@@ -16,18 +16,19 @@
  */
 package org.apache.sis.filter.sqlmm;
 
+import java.util.EnumMap;
 import java.util.Optional;
 import javax.measure.Quantity;
 import org.opengis.geometry.Envelope;
+import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryType;
+import org.apache.sis.filter.visitor.FunctionIdentifier;
 import static org.apache.sis.geometry.wrapper.GeometryType.*;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import java.util.EnumMap;
 import org.opengis.filter.SpatialOperatorName;
 import org.opengis.filter.capability.AvailableFunction;
-import org.apache.sis.setup.GeometryLibrary;
 
 
 /**
@@ -39,7 +40,7 @@ import org.apache.sis.setup.GeometryLibrary;
  *
  * @see <a href="https://www.iso.org/standard/60343.html";>ISO 13249-3 - 
SQLMM</a>
  */
-public enum SQLMM {
+public enum SQLMM implements FunctionIdentifier {
     /**
      * The number of dimensions in the geometry.
      */
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/FunctionIdentifier.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/FunctionIdentifier.java
new file mode 100644
index 0000000000..6c2c6adcc4
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/FunctionIdentifier.java
@@ -0,0 +1,56 @@
+/*
+ * 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.visitor;
+
+import java.sql.Types;
+
+
+/**
+ * Identification of a function. Instances of this interface are enumeration 
values or
+ * code list values used by factories for identifying the filters or 
expressions to create.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public interface FunctionIdentifier {
+    /**
+     * Returns the name of the function.
+     *
+     * @return the function name.
+     */
+    String name();
+
+    /**
+     * Returns the types of arguments and the type of the return value of this 
function.
+     * The {@code dataTypes[0]} array element is the data type of the 
function's return value.
+     * All other array elements are the data types of the function's 
parameters, in order.
+     *
+     * <p>The values of all array elements are constants of the {@link Types} 
class.
+     * Permitted values are: {@link Types#DOUBLE}, {@link Types#BOOLEAN}.</p>
+     *
+     * <p>This method is invoked for checking if a filter or expression in 
Java code can be replaced by
+     * a <abbr>SQL</abbr> function. If this method returns {@code null}, then 
the function signature will
+     * not be verified.</p>
+     *
+     * @todo We may change the return type to {@code int[][]} in a future 
version if we want
+     *       to allow function overloading (same function name but with 
different arguments).
+     *
+     * @return  dataTypes  data type of the return value followed by 
parameters, as {@link java.sql.Types} constants.
+     */
+    default int[] getSignature() {
+        return null;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/FunctionNames.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/FunctionNames.java
index ad4a3bd834..d974cd1fd4 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/FunctionNames.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/FunctionNames.java
@@ -27,7 +27,6 @@ import org.apache.sis.util.internal.shared.CollectionsExt;
 
 /**
  * Names of some filters and expressions used in Apache <abbr>SIS</abbr>.
- * This class defines only the names that need to be referenced from at least 
two different classes.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -50,6 +49,9 @@ public final class FunctionNames {
     /** Value of {@link org.opengis.filter.ValueReference#getFunctionName()}. 
*/
     public static final String ValueReference = "ValueReference";
 
+    /** Synonymous of {@link #ValueReference} used in Filter Encoding XML. */
+    public static final String PropertyName = "PropertyName";
+
     /** The "Add" (+) arithmetic expression. */
     public static final String Add = "Add";
 
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/Visitor.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/Visitor.java
index fc21c0d035..221167ca67 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/Visitor.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/visitor/Visitor.java
@@ -55,20 +55,20 @@ import org.opengis.filter.ComparisonOperatorName;
  */
 public abstract class Visitor<R,A> {
     /**
-     * All filters known to this visitor.
-     * May contain an entry associated to the {@code null} key.
+     * All filters known to this visitor. May contain an entry associated to 
the {@code null} key,
+     * which specifies the action to execute when a {@link Filter} instance is 
null or has a null type.
      *
      * @see #setFilterHandler(CodeList, BiConsumer)
      */
-    private final Map<CodeList<?>, BiConsumer<Filter<R>,A>> filters;
+    protected final Map<CodeList<?>, BiConsumer<Filter<R>, A>> filters;
 
     /**
-     * All expressions known to this visitor.
-     * May contain an entry associated to the {@code null} key.
+     * All expressions known to this visitor. May contain an entry associated 
to the {@code null} key,
+     * which specifies the action to execute when an {@link Expression} 
instance is null.
      *
      * @see #setExpressionHandler(String, BiConsumer)
      */
-    private final Map<String, BiConsumer<Expression<R,?>,A>> expressions;
+    protected final Map<String, BiConsumer<Expression<R,?>, A>> expressions;
 
     /**
      * Creates a new visitor.
@@ -100,6 +100,7 @@ public abstract class Visitor<R,A> {
      * @param  copyExpressions  whether to copy the map of expression handlers.
      *
      * @see #removeFilterHandlers(Collection)
+     * @see #removeFunctionHandlers(Collection)
      */
     protected Visitor(final Visitor<R,A> source, final boolean copyFilters, 
final boolean copyExpressions) {
         filters     = copyFilters     ? new HashMap<>(source.filters)     : 
source.filters;
@@ -114,7 +115,7 @@ public abstract class Visitor<R,A> {
      * @param  type  identification of the filter type (can be {@code null}).
      * @return the action to execute when the identified filter is found, or 
{@code null} if none.
      */
-    protected final BiConsumer<Filter<R>,A> getFilterHandler(final CodeList<?> 
type) {
+    protected final BiConsumer<Filter<R>, A> getFilterHandler(final 
CodeList<?> type) {
         return filters.get(type);
     }
 
@@ -126,7 +127,7 @@ public abstract class Visitor<R,A> {
      * @param  type  identification of the expression type (can be {@code 
null}).
      * @return the action to execute when the identified expression is found, 
or {@code null} if none.
      */
-    protected final BiConsumer<Expression<R,?>,A> getExpressionHandler(final 
String type) {
+    protected final BiConsumer<Expression<R,?>, A> getExpressionHandler(final 
String type) {
         return expressions.get(type);
     }
 
@@ -138,7 +139,7 @@ public abstract class Visitor<R,A> {
      * @param  type    identification of the filter type (can be {@code null}).
      * @param  action  the action to execute when the identified filter is 
found.
      */
-    protected final void setFilterHandler(final CodeList<?> type, final 
BiConsumer<Filter<R>,A> action) {
+    protected final void setFilterHandler(final CodeList<?> type, final 
BiConsumer<Filter<R>, A> action) {
         filters.put(type, action);
     }
 
@@ -149,7 +150,7 @@ public abstract class Visitor<R,A> {
      * @param  lastType  identification of the last filter type (inclusive).
      * @param  action    the action to execute when an identified filter is 
found.
      */
-    private void setFamilyHandlers(final CodeList<?> lastType, final 
BiConsumer<Filter<R>,A> action) {
+    private void setFamilyHandlers(final CodeList<?> lastType, final 
BiConsumer<Filter<R>, A> action) {
         for (final CodeList<?> type : lastType.family()) {
             filters.put(type, action);
             if (type == lastType) break;
@@ -164,7 +165,7 @@ public abstract class Visitor<R,A> {
      * @param  type    identification of the expression type (can be {@code 
null}).
      * @param  action  the action to execute when the identified expression is 
found.
      */
-    protected final void setExpressionHandler(final String type, final 
BiConsumer<Expression<R,?>,A> action) {
+    protected final void setExpressionHandler(final String type, final 
BiConsumer<Expression<R,?>, A> action) {
         expressions.put(type, action);
     }
 
@@ -174,7 +175,7 @@ public abstract class Visitor<R,A> {
      * @param  types   identification of the expression types.
      * @param  action  the action to execute when the identified expression is 
found.
      */
-    private void setExpressionHandlers(final BiConsumer<Expression<R,?>,A> 
action, final String... types) {
+    private void setExpressionHandlers(final BiConsumer<Expression<R,?>, A> 
action, final String... types) {
         for (final String type : types) {
             expressions.put(type, action);
         }
@@ -185,7 +186,7 @@ public abstract class Visitor<R,A> {
      *
      * @param  action  the action to execute when one of the enumerated 
filters is found.
      */
-    protected final void setLogicalHandlers(final BiConsumer<Filter<R>,A> 
action) {
+    protected final void setLogicalHandlers(final BiConsumer<Filter<R>, A> 
action) {
         setFamilyHandlers(LogicalOperatorName.NOT, action);
     }
 
@@ -194,7 +195,7 @@ public abstract class Visitor<R,A> {
      *
      * @param  action  the action to execute when one of the enumerated 
filters is found.
      */
-    protected final void setNullAndNilHandlers(final BiConsumer<Filter<R>,A> 
action) {
+    protected final void setNullAndNilHandlers(final BiConsumer<Filter<R>, A> 
action) {
         
setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_NULL),
 action);
         
setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_NIL), 
 action);
     }
@@ -204,7 +205,7 @@ public abstract class Visitor<R,A> {
      *
      * @param  action  the action to execute when one of the enumerated 
filters is found.
      */
-    protected final void setBinaryComparisonHandlers(final 
BiConsumer<Filter<R>,A> action) {
+    protected final void setBinaryComparisonHandlers(final 
BiConsumer<Filter<R>, A> action) {
         
setFamilyHandlers(ComparisonOperatorName.PROPERTY_IS_GREATER_THAN_OR_EQUAL_TO, 
action);
     }
 
@@ -216,7 +217,7 @@ public abstract class Visitor<R,A> {
      *
      * @param  action  the action to execute when one of the enumerated 
filters is found.
      */
-    protected final void setBinaryTemporalHandlers(final 
BiConsumer<Filter<R>,A> action) {
+    protected final void setBinaryTemporalHandlers(final BiConsumer<Filter<R>, 
A> action) {
         setFamilyHandlers(TemporalOperatorName.ANY_INTERACTS, action);
     }
 
@@ -228,7 +229,7 @@ public abstract class Visitor<R,A> {
      *
      * @param  action  the action to execute when one of the enumerated 
filters is found.
      */
-    protected final void setSpatialHandlers(final BiConsumer<Filter<R>,A> 
action) {
+    protected final void setSpatialHandlers(final BiConsumer<Filter<R>, A> 
action) {
         setFamilyHandlers(SpatialOperatorName.OVERLAPS, action);
         setFamilyHandlers(DistanceOperatorName.WITHIN,  action);
     }
@@ -238,19 +239,30 @@ public abstract class Visitor<R,A> {
      *
      * @param  action  the action to execute when one of the enumerated 
expressions is found.
      */
-    protected final void setMathHandlers(final BiConsumer<Expression<R,?>,A> 
action) {
+    protected final void setMathHandlers(final BiConsumer<Expression<R,?>, A> 
action) {
         setExpressionHandlers(action, FunctionNames.Add, 
FunctionNames.Subtract, FunctionNames.Multiply, FunctionNames.Divide);
     }
 
     /**
-     * Removes all filters of the given types. Types that have no registered 
handlers are ignored.
+     * Removes all filter handlers of the given types. Types that have no 
registered handlers are ignored.
+     * This is equivalent to {@code filters.keySet().removeAll(types)} but 
more type safe.
      *
-     * @param  types  types of filters to remove.
+     * @param  types  types of filter handlers to remove.
      */
     protected final void removeFilterHandlers(final Collection<? extends 
CodeList<?>> types) {
         filters.keySet().removeAll(types);
     }
 
+    /**
+     * Removes all function handlers of the given names. Names that have no 
registered handlers are ignored.
+     * This is equivalent to {@code expressions.keySet().removeAll(names)} but 
more type safe.
+     *
+     * @param  names  names of expression handlers to remove.
+     */
+    protected final void removeFunctionHandlers(final Collection<String> 
names) {
+        expressions.keySet().removeAll(names);
+    }
+
     /**
      * Executes the registered action for the given filter.
      * Actions are registered by calls to {@code setFooHandler(…)} before the 
call to this {@code visit(…)} method.
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Reflection.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Reflection.java
index 4a8dbf1b01..84bc643ec2 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Reflection.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Reflection.java
@@ -69,6 +69,13 @@ public final class Reflection {
      */
     public static final String COLUMN_NAME = "COLUMN_NAME";
 
+    /**
+     * The {@value} key for getting a column type. Possible values are {@code 
functionColumnUnknown},
+     * {@code functionColumnIn}, {@code functionColumnInOut}, {@code 
functionColumnOut}, {@code functionReturn}
+     * or {@code functionColumnResult}.
+     */
+    public static final String COLUMN_TYPE = "COLUMN_TYPE";
+
     /**
      * The {@value} key for getting the data type as one of {@link 
java.sql.Types} constants.
      *
@@ -81,6 +88,21 @@ public final class Reflection {
      */
     public static final String TYPE_NAME = "TYPE_NAME";
 
+    /**
+     * The {@value} key for getting a function name.
+     */
+    public static final String FUNCTION_NAME = "FUNCTION_NAME";
+
+    /**
+     * The {@value} key for getting a unique function name, including among 
overload variants.
+     */
+    public static final String SPECIFIC_NAME = "SPECIFIC_NAME";
+
+    /**
+     * The {@value} key for getting the ordinal position of a function 
parameter.
+     */
+    public static final String ORDINAL_POSITION = "ORDINAL_POSITION";
+
     /**
      * The {@value} key for the size for of a column. For numeric data, this 
is the maximum precision.
      * For character data, this is the length in characters.
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/TypeMapper.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/TypeMapper.java
index b9df12b181..74ae288d4b 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/TypeMapper.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/TypeMapper.java
@@ -17,7 +17,6 @@
 package org.apache.sis.metadata.sql.internal.shared;
 
 import java.util.Date;
-import java.sql.Types;
 import java.sql.JDBCType;
 
 
@@ -52,7 +51,7 @@ final class TypeMapper {
     private final Class<?> classe;
 
     /**
-     * A constant from the SQL {@link Types} enumeration.
+     * The data type.
      */
     private final JDBCType type;
 
diff --git 
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
index e19880fcdc..72ed0d563b 100644
--- 
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
@@ -38,26 +38,30 @@ final class ExtendedClauseWriter extends 
SelectionClauseWriter {
      * Creates a new converter from filters/expressions to SQL.
      */
     private ExtendedClauseWriter() {
-        super(DEFAULT);
+        super(DEFAULT, true, false);
         setFilterHandler(SpatialOperatorName.BBOX, 
getFilterHandler(SpatialOperatorName.INTERSECTS));
     }
 
     /**
      * Creates a new converter initialized to the same handlers as the 
specified converter.
      *
-     * @param  source  the converter from which to copy the handlers.
+     * @param  source           the converter from which to copy the handlers.
+     * @param  copyFilters      whether to copy the map of filter handlers.
+     * @param  copyExpressions  whether to copy the map of expression handlers.
      */
-    private ExtendedClauseWriter(ExtendedClauseWriter source) {
-        super(source);
+    private ExtendedClauseWriter(ExtendedClauseWriter source, boolean 
copyFilters, boolean copyExpressions) {
+        super(source, copyFilters, copyExpressions);
     }
 
     /**
      * Creates a new converter of the same class as {@code this} and 
initialized with the same data.
      *
+     * @param  copyFilters      whether to copy the map of filter handlers.
+     * @param  copyExpressions  whether to copy the map of expression handlers.
      * @return a converter initialized to a copy of {@code this}.
      */
     @Override
-    protected SelectionClauseWriter duplicate() {
-        return new ExtendedClauseWriter(this);
+    protected SelectionClauseWriter duplicate(boolean copyFilters, boolean 
copyExpressions) {
+        return new ExtendedClauseWriter(this, copyFilters, copyExpressions);
     }
 }
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 b108cbe6c2..a2d748186c 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
@@ -17,16 +17,20 @@
 package org.apache.sis.storage.sql.feature;
 
 import java.util.List;
+import java.util.HashSet;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.function.BiConsumer;
+import java.sql.Types;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import org.apache.sis.filter.base.XPathSource;
+import org.apache.sis.filter.visitor.FunctionIdentifier;
 import org.apache.sis.filter.visitor.FunctionNames;
 import org.apache.sis.filter.visitor.Visitor;
+import org.apache.sis.metadata.sql.internal.shared.Reflection;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.util.CodeList;
@@ -107,14 +111,18 @@ public class SelectionClauseWriter extends 
Visitor<Feature, SelectionClause> {
         /*
          * Spatial filters.
          */
-        setFilterHandler(SpatialOperatorName.CONTAINS,   new 
Function(FunctionNames.ST_Contains));
-        setFilterHandler(SpatialOperatorName.CROSSES,    new 
Function(FunctionNames.ST_Crosses));
-        setFilterHandler(SpatialOperatorName.DISJOINT,   new 
Function(FunctionNames.ST_Disjoint));
-        setFilterHandler(SpatialOperatorName.EQUALS,     new 
Function(FunctionNames.ST_Equals));
-        setFilterHandler(SpatialOperatorName.INTERSECTS, new 
Function(FunctionNames.ST_Intersects));
-        setFilterHandler(SpatialOperatorName.OVERLAPS,   new 
Function(FunctionNames.ST_Overlaps));
-        setFilterHandler(SpatialOperatorName.TOUCHES,    new 
Function(FunctionNames.ST_Touches));
-        setFilterHandler(SpatialOperatorName.WITHIN,     new 
Function(FunctionNames.ST_Within));
+        setFilterHandler(SpatialOperatorName.CONTAINS,   new 
SpatialFilter(FunctionNames.ST_Contains));
+        setFilterHandler(SpatialOperatorName.CROSSES,    new 
SpatialFilter(FunctionNames.ST_Crosses));
+        setFilterHandler(SpatialOperatorName.DISJOINT,   new 
SpatialFilter(FunctionNames.ST_Disjoint));
+        setFilterHandler(SpatialOperatorName.EQUALS,     new 
SpatialFilter(FunctionNames.ST_Equals));
+        setFilterHandler(SpatialOperatorName.INTERSECTS, new 
SpatialFilter(FunctionNames.ST_Intersects));
+        setFilterHandler(SpatialOperatorName.OVERLAPS,   new 
SpatialFilter(FunctionNames.ST_Overlaps));
+        setFilterHandler(SpatialOperatorName.TOUCHES,    new 
SpatialFilter(FunctionNames.ST_Touches));
+        setFilterHandler(SpatialOperatorName.WITHIN,     new 
SpatialFilter(FunctionNames.ST_Within));
+        /*
+         * Mathematical functions.
+         */
+        addAllOf(org.apache.sis.filter.math.Function.class);
         /*
          * Expression visitor.
          */
@@ -124,18 +132,30 @@ public class SelectionClauseWriter extends 
Visitor<Feature, SelectionClause> {
         setExpressionHandler(FunctionNames.Multiply, new Arithmetic(" * "));
         setExpressionHandler(FunctionNames.Literal, (e,sql) -> 
sql.appendLiteral(((Literal<Feature,?>) e).getValue()));
         setExpressionHandler(FunctionNames.ValueReference, (e,sql) -> 
sql.appendColumnName(((ValueReference<Feature,?>) e).getXPath()));
-        // Filters created from Filter Encoding XML may specify "PropertyName" 
instead of "Value reference".
-        setExpressionHandler("PropertyName", 
getExpressionHandler(FunctionNames.ValueReference));
+        setExpressionHandler(FunctionNames.PropertyName, 
getExpressionHandler(FunctionNames.ValueReference));
+    }
+
+    /**
+     * Adds all values defined in the given enumeration as functions.
+     */
+    private void addAllOf(final Class<? extends Enum<?>> functions) {
+        for (Enum<?> id : functions.getEnumConstants()) {
+            final String name = id.name();
+            setExpressionHandler(name, new Function(id));
+        }
     }
 
     /**
      * Creates a new converter initialized to the same handlers as the 
specified converter.
+     * This constructor is for implementations of {@link #duplicate(boolean, 
boolean)}.
      * The given source is usually {@link #DEFAULT}.
      *
-     * @param  source  the converter from which to copy the handlers.
+     * @param  source           the converter from which to copy the handlers.
+     * @param  copyFilters      whether to copy the map of filter handlers.
+     * @param  copyExpressions  whether to copy the map of expression handlers.
      */
-    protected SelectionClauseWriter(final SelectionClauseWriter source) {
-        super(source, true, false);
+    protected SelectionClauseWriter(SelectionClauseWriter source, boolean 
copyFilters, boolean copyExpressions) {
+        super(source, copyFilters, copyExpressions);
     }
 
     /**
@@ -143,10 +163,12 @@ public class SelectionClauseWriter extends 
Visitor<Feature, SelectionClause> {
      * This method is invoked before to remove handlers for functions that are 
unsupported on the target
      * database software.
      *
+     * @param  copyFilters      whether to copy the map of filter handlers.
+     * @param  copyExpressions  whether to copy the map of expression handlers.
      * @return a converter initialized to a copy of {@code this}.
      */
-    protected SelectionClauseWriter duplicate() {
-        return new SelectionClauseWriter(this);
+    protected SelectionClauseWriter duplicate(boolean copyFilters, boolean 
copyExpressions) {
+        return new SelectionClauseWriter(this, copyFilters, copyExpressions);
     }
 
     /**
@@ -159,58 +181,116 @@ public class SelectionClauseWriter extends 
Visitor<Feature, SelectionClause> {
      * @return a writer with unsupported functions removed.
      */
     final SelectionClauseWriter removeUnsupportedFunctions(final Database<?> 
database) {
-        final var unsupported = new HashMap<String, SpatialOperatorName>();
+        boolean failure = false;
+        final var unsupportedFilters = new HashMap<String, CodeList<?>>(16);
+        final var unsupportedExpressions = new HashSet<String>();
         final var accessors = GeometryEncoding.initial();
         try (Connection c = database.source.getConnection()) {
             final DatabaseMetaData metadata = c.getMetaData();
-            /*
-             * Get the names of all spatial functions for which a handler is 
registered.
-             * All those handlers should be instances of `Function`, otherwise 
we do not
-             * know how to determine whether the function is supported or not.
-             */
             final boolean lowerCase = metadata.storesLowerCaseIdentifiers();
             final boolean upperCase = metadata.storesUpperCaseIdentifiers();
-            for (final SpatialOperatorName type : 
SpatialOperatorName.values()) {
-                final BiConsumer<Filter<Feature>, SelectionClause> function = 
getFilterHandler(type);
-                if (function instanceof Function) {
-                    String name = ((Function) function).name;
+            /*
+             * Get the names of all spatial filters for which a handler is 
registered.
+             * These filters are initially assumed unsupported by the target 
database.
+             * We then iterate over the (potentially large) list of supported 
filters
+             * for removing from the map all the filters that we found 
supported.
+             * After that loop, only truly unsupported items should be 
remaining.
+             */
+            for (final SpatialOperatorName id : SpatialOperatorName.values()) {
+                final BiConsumer<Filter<Feature>, SelectionClause> handler = 
getFilterHandler(id);
+                if (handler instanceof SpatialFilter) {
+                    String name = ((SpatialFilter) handler).name;
                     if (lowerCase) name = name.toLowerCase(Locale.US);
                     if (upperCase) name = name.toUpperCase(Locale.US);
-                    unsupported.put(name, type);
+                    unsupportedFilters.put(name, id);
                 }
             }
-            /*
-             * Remove from above map all functions that are supported by the 
database.
-             * This list is potentially large so we do not put those items in 
a map.
-             */
             final String prefix = database.escapeWildcards(lowerCase ? "st_" : 
"ST_");
             try (ResultSet r = 
metadata.getFunctions(database.catalogOfSpatialTables,
                                                      
database.schemaOfSpatialTables,
                                                      prefix + '%'))
             {
                 while (r.next()) {
-                    final String function = r.getString("FUNCTION_NAME");
+                    String function = r.getString(Reflection.FUNCTION_NAME);
                     GeometryEncoding.checkSupport(accessors, function);
-                    unsupported.remove(function);
+                    unsupportedFilters.remove(function);
                 }
             }
-        } catch (SQLException e) {
             /*
-             * If this exception happens before `unsupported` entries were 
removed,
-             * this is equivalent to assuming that all functions are 
unsupported.
+             * Iterate over all functions (math, etc.) for which a handler is 
registered.
+             * For each of these function, get the parameter types and return 
value type.
+             * We check if a function is supported not only by searching for 
its name,
+             * but also by checking the arguments.
              */
+            for (final var entry : expressions.entrySet()) {
+                final BiConsumer<Expression<Feature,?>, SelectionClause> 
handler = entry.getValue();
+                if (handler instanceof Function) {
+                    final Enum<?> id = ((Function) handler).function;
+                    if (id instanceof FunctionIdentifier) {
+                        final int[] signature = ((FunctionIdentifier) 
id).getSignature();   // May be null.
+                        boolean isSupported = false;
+                        String specificName = "";
+                        String name = id.name();
+                        if (lowerCase) name = name.toLowerCase(Locale.US);
+                        if (upperCase) name = name.toUpperCase(Locale.US);
+                        try (ResultSet r = metadata.getFunctionColumns(null, 
null, name, "%")) {
+                            while (r.next()) {
+                                if (!specificName.equals(specificName = 
r.getString(Reflection.SPECIFIC_NAME))) {
+                                    if (isSupported) break;     // Found a 
supported variant of the function.
+                                    isSupported = true;
+                                } else if (!isSupported) {
+                                    continue;   // Continue the search for the 
next overload variant.
+                                }
+                                switch (r.getShort(Reflection.COLUMN_TYPE)) {
+                                    case DatabaseMetaData.functionColumnIn:
+                                    case DatabaseMetaData.functionReturn: {
+                                        if (signature == null) continue;
+                                        final int n = 
r.getInt(Reflection.ORDINAL_POSITION);
+                                        if (n >= 0 && n < signature.length) {
+                                            int type = 
r.getInt(Reflection.DATA_TYPE);
+                                            switch (type) {
+                                                case Types.SMALLINT:  // Derby 
does not support `TINYINT`.
+                                                case Types.TINYINT:
+                                                case Types.BIT:   type = 
Types.BOOLEAN; break;
+                                                case Types.REAL:
+                                                case Types.FLOAT: type = 
Types.DOUBLE; break;
+                                            }
+                                            if (signature[n] == type) continue;
+                                        }
+                                    }
+                                }
+                                isSupported = false;
+                                // Continue because the `ResultSet` may return 
many overload variants.
+                            }
+                        }
+                        if (!isSupported) {
+                            unsupportedExpressions.add(entry.getKey());
+                        }
+                    }
+                }
+            }
+        } catch (SQLException e) {
             database.listeners.warning(e);
+            failure = true;
         }
-        database.setGeometryEncodingFunctions(accessors);
         /*
-         * Remaining functions are unsupported functions.
+         * The remaining items in the `unsupported` collection are functions 
that are unsupported by the database.
+         * If this collection is empty, then all functions are supported and 
we can use `this` with no change.
          */
-        if (unsupported.isEmpty()) {
-            return this;
+        database.setGeometryEncodingFunctions(accessors);
+        final boolean copyFilters     = failure || 
!unsupportedFilters.isEmpty();
+        final boolean copyExpressions = failure || 
!unsupportedExpressions.isEmpty();
+        if (copyFilters | copyExpressions) {
+            final SelectionClauseWriter copy = duplicate(copyFilters, 
copyExpressions);
+            copy.removeFilterHandlers(unsupportedFilters.values());
+            copy.removeFunctionHandlers(unsupportedExpressions);
+            if (failure) {
+                copy.filters.values().removeIf((handler) -> handler instanceof 
SpatialFilter);
+                copy.expressions.values().removeIf((handler) -> handler 
instanceof Function);
+            }
+            return copy;
         }
-        final SelectionClauseWriter copy = duplicate();
-        copy.removeFilterHandlers(unsupported.values());
-        return copy;
+        return this;
     }
 
     /**
@@ -399,17 +479,42 @@ 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. Callers should check 
if this is
-     * the case by invoking {@link SelectionClause#isInvalid()} after this 
method call.
+     * Handler for a function with an arbitrary number of parameters 
(potentially zero).
+     * This handler stops immediately if a parameter cannot be expressed in 
<abbr>SQL</abbr>,
+     * leaving the trailing part of the <abbr>SQL</abbr> 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<Expression<Feature,?>, 
SelectionClause> {
+        /** Identification of the function. */
+        final Enum<?> function;
+
+        /** Creates a function. */
+        Function(final Enum<?> function) {
+            this.function = function;
+        }
+
+        /** Invoked when an expression should be converted to a 
<abbr>SQL</abbr> clause. */
+        @Override public void accept(final Expression<Feature,?> expression, 
final SelectionClause sql) {
+            sql.appendSpatialFunction(function.name());
+            writeParameters(sql, expression.getParameters(), ", ", false);
+        }
+    }
+
+
+
+
+    /**
+     * Appends a spatial function name followed by an arbitrary number of 
parameters (potentially zero).
+     * This method stops immediately if a parameter cannot be expressed in 
<abbr>SQL</abbr>, leaving the
+     * trailing part of the <abbr>SQL</abbr> 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. */
+    private final class SpatialFilter implements BiConsumer<Filter<Feature>, 
SelectionClause> {
+        /** Name of the function. */
         final String name;
 
         /** Creates a function of the given name. */
-        Function(final String name) {
+        SpatialFilter(final String name) {
             this.name = name;
         }
 
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 0e0ee0504b..d24d535395 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
@@ -43,7 +43,7 @@ final class ExtendedClauseWriter extends 
SelectionClauseWriter {
      * Creates a new converter from filters/expressions to SQL.
      */
     private ExtendedClauseWriter() {
-        super(DEFAULT);
+        super(DEFAULT, true, false);
         setFilterHandler(SpatialOperatorName.BBOX, (f,sql) -> {
             writeBinaryOperator(sql, f, " && ");
         });
@@ -52,19 +52,23 @@ final class ExtendedClauseWriter extends 
SelectionClauseWriter {
     /**
      * Creates a new converter initialized to the same handlers as the 
specified converter.
      *
-     * @param  source  the converter from which to copy the handlers.
+     * @param  source           the converter from which to copy the handlers.
+     * @param  copyFilters      whether to copy the map of filter handlers.
+     * @param  copyExpressions  whether to copy the map of expression handlers.
      */
-    private ExtendedClauseWriter(ExtendedClauseWriter source) {
-        super(source);
+    private ExtendedClauseWriter(ExtendedClauseWriter source, boolean 
copyFilters, boolean copyExpressions) {
+        super(source, copyFilters, copyExpressions);
     }
 
     /**
      * Creates a new converter of the same class as {@code this} and 
initialized with the same data.
      *
+     * @param  copyFilters      whether to copy the map of filter handlers.
+     * @param  copyExpressions  whether to copy the map of expression handlers.
      * @return a converter initialized to a copy of {@code this}.
      */
     @Override
-    protected SelectionClauseWriter duplicate() {
-        return new ExtendedClauseWriter(this);
+    protected SelectionClauseWriter duplicate(boolean copyFilters, boolean 
copyExpressions) {
+        return new ExtendedClauseWriter(this, copyFilters, copyExpressions);
     }
 }
diff --git 
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
index 35a30f8961..71b460e328 100644
--- 
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
@@ -190,6 +190,7 @@ public final class SQLStoreTest extends TestOnAllDatabases {
              */
             verifySimpleQuerySorting(store);
             verifySimpleQueryWithLimit(store);
+            verifySimpleQueryWithMath(store);
             verifyWhereResourceId(store);
             verifySimpleWhere(store);
             verifyWhereOnLink(store);
@@ -387,6 +388,34 @@ public final class SQLStoreTest extends TestOnAllDatabases 
{
         assertEquals(2, subset.features(false).count());
     }
 
+    /**
+     * Requests features with a mathematical operation.
+     *
+     * @param  dataset  the store on which to query the features.
+     * @throws DataStoreException if an error occurred during query execution.
+     */
+    private void verifySimpleQueryWithMath(final SimpleFeatureStore dataset) 
throws DataStoreException {
+        final FeatureSet  cities = dataset.findResource("Cities");
+        final FeatureQuery query = new FeatureQuery();
+        query.setProjection(new 
FeatureQuery.NamedExpression(FF.property("english_name")),
+                            new 
FeatureQuery.NamedExpression(FF.function("SQRT", FF.property("population")), 
"value"));
+        final FeatureSet subset = cities.subset(query);
+        final var values = new HashMap<Object, Object>();
+        subset.features(false).forEach(
+                (feature) -> 
assertNull(values.put(feature.getPropertyValue("english_name"),
+                                                   
feature.getPropertyValue("value"))));
+        final String[] expected = {"Montreal", "Quebec", "Paris", "Tōkyō"};
+        final int[]  population = {  1704694,   531902, 2206488, 13622267};
+        for (int i=0; i<expected.length; i++) {
+            final String city  = expected[i];
+            final double value = assertInstanceOf(Double.class, 
values.remove(city), city);
+            assertEquals(population[i], value * value, 0.1, city);
+        }
+        // Try again with a function returning a boolean value.
+        query.setProjection(new 
FeatureQuery.NamedExpression(FF.function("IS_NAN", FF.property("population")), 
"flag"));
+        cities.subset(query).features(false).forEach((feature) -> 
assertEquals(Boolean.FALSE, feature.getPropertyValue("flag")));
+    }
+
     /**
      * Requests a new set of features filtered by an identifier.
      *
@@ -482,8 +511,8 @@ public final class SQLStoreTest extends TestOnAllDatabases {
 
     /**
      * Checks that operations stacked on feature stream are well executed.
-     * This test focuses on mapping and peeking actions overloaded by SQL 
streams.
-     * Operations used here are meaningless; we just want to ensure that the 
pipeline does not skip any operation.
+     * This test focuses on mapping and peeking actions overloaded by 
<abbr>SQL</abbr> streams.
+     * Operations used here are meaningless, we just want to ensure that the 
pipeline does not skip any operation.
      *
      * @param  cities  a feature set containing all cities defined for the 
test class.
      */
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java
 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java
index 25fbc34507..d9919b26ae 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java
@@ -74,7 +74,7 @@ public final class SelectionClauseWriterTest extends TestCase 
implements SchemaM
     public void testOnDerby() throws Exception {
         try (TestDatabase db = TestDatabase.create("SelectionClause")) {
             db.executeSQL(List.of("CREATE TABLE TEST (ALPHA INTEGER, BETA 
INTEGER, GAMMA INTEGER, PI FLOAT);"));
-            final StorageConnector connector = new StorageConnector(db.source);
+            final var connector = new StorageConnector(db.source);
             connector.setOption(SchemaModifier.OPTION_KEY, this);
             try (DataStore store = new SQLStoreProvider().open(connector)) {
                 table = (Table) store.findResource("TEST");
@@ -126,11 +126,11 @@ public final class SelectionClauseWriterTest extends 
TestCase implements SchemaM
      * Verifies that a spatial operator transforms literal value before-hand 
if possible.
      */
     private void testGeometricFilterWithTransform() {
-        final GeneralEnvelope bbox = new 
GeneralEnvelope(HardCodedCRS.WGS84_LATITUDE_FIRST);
+        final var bbox = new 
GeneralEnvelope(HardCodedCRS.WGS84_LATITUDE_FIRST);
         bbox.setEnvelope(-10, 20, -5, 25);
 
         Filter<Feature> filter = FF.intersects(FF.property("BETA"), 
FF.literal(bbox));
-        final Optimization optimization = new Optimization();
+        final var optimization = new Optimization();
         optimization.setFeatureType(table.featureType);
         verifySQL(optimization.apply(filter), "ST_Intersects(\"BETA\", " +
                 "ST_GeomFromText('POLYGON ((20 -10, 25 -10, 25 -5, 20 -5, 20 
-10))'))");
@@ -141,7 +141,7 @@ public final class SelectionClauseWriterTest extends 
TestCase implements SchemaM
      * and verifies that the result is equal to the expected string.
      */
     private void verifySQL(final Filter<Feature> filter, final String 
expected) {
-        final SelectionClause sql = new SelectionClause(table);
+        final var sql = new SelectionClause(table);
         assertTrue(sql.tryAppend(SelectionClauseWriter.DEFAULT, filter));
         assertEquals(expected, sql.toString());
     }

Reply via email to