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()); }
