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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 11b9c21066 Bug fix: when `FeatureQuery` contains a `projection` item 
which is an operation, and when one of the operation parameters is another 
operation (typically a link), the operation was not executed.
11b9c21066 is described below

commit 11b9c21066c2110463f6a68d16bac677ac88d822
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Sep 15 15:07:53 2025 +0200

    Bug fix: when `FeatureQuery` contains a `projection` item which is an 
operation,
    and when one of the operation parameters is another operation (typically a 
link),
    the operation was not executed.
---
 .../sis/feature/builder/OperationWrapper.java      |  2 +-
 .../feature/privy/FeatureProjectionBuilder.java    | 33 ++++++++-
 .../org/apache/sis/filter/ConvertFunction.java     |  2 +-
 .../sis/filter/internal/GeometryFromFeature.java   |  2 +-
 .../main/org/apache/sis/filter/internal/Node.java  |  8 +++
 .../apache/sis/filter/sqlmm/SpatialFunction.java   |  6 +-
 .../sis/storage/sql/postgis/PostgresTest.java      | 83 +++++++++++++++++++++-
 7 files changed, 126 insertions(+), 10 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
index 7a6a656f7e..9ca57b576a 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
@@ -28,7 +28,7 @@ import org.opengis.feature.PropertyType;
 
 /**
  * Wraps an existing operation. This package cannot create new operations, 
except for a few special cases.
- * The user need to specify fully formed objects.
+ * The user needs to specify fully formed objects.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
index 51ab93d85f..49858e1f63 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
@@ -142,6 +142,14 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
      */
     private final Map<String, List<Item>> dependencies;
 
+    /**
+     * Whether to store operations as attributes. By default, when a 
{@linkplain #addSourceProperty source
+     * property is added in the projection}, operation are forwarded as given 
(with their dependencies).
+     * But if this flag is set to {@code true}, then operations are replaced 
by an attribute.
+     * Then, it will be caller's responsibility to store the value.
+     */
+    private boolean operationResultAsAttribute;
+
     /**
      * Creates a new builder instance using the default factories.
      *
@@ -177,7 +185,7 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
      *
      * @param  childType   the feature type to use.
      * @param  expression  the expression from which to get the expected type.
-     * @return the expected type, or {@code null}.
+     * @return handler for the property, or {@code null} if it cannot be 
resolved.
      *
      * @see FeatureExpression#expectedType(FeatureProjectionBuilder)
      */
@@ -194,6 +202,27 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
         }
     }
 
+    /**
+     * Adds a property from the source feature type, but replacing operation 
results by attributes.
+     * This method is invoked when an operation uses a source property as a 
template, usually because
+     * the result will be of the same type as one of the operation argument 
(usually the first argument).
+     * In such case, the caller does not want the operation to be executed, 
since the property will rather
+     * be used as a slot for receiving the result.
+     *
+     * @param  childType   the feature type to use.
+     * @param  expression  the expression from which to get the expected type.
+     * @return handler for the property, or {@code null} if it cannot be 
resolved.
+     */
+    public Item addTemplateProperty(final FeatureExpression<?,?> expression) {
+        final boolean status = operationResultAsAttribute;
+        try {
+            operationResultAsAttribute = true;
+            return expression.expectedType(this);
+        } finally {
+            operationResultAsAttribute = status;
+        }
+    }
+
     /**
      * Adds the given property, replacing operation by an attribute storing 
the operation result.
      * This method may return {@code null} if it cannot resolve the property 
type, in which case
@@ -259,7 +288,7 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
             reserve(property.getName(), null);
             deferred = new ArrayList<>();
             builder = addPropertyResult(property, deferred);
-        } else if (property instanceof AbstractOperation) {
+        } else if (!operationResultAsAttribute && property instanceof 
AbstractOperation) {
             /*
              * For operations, remember the dependencies in order to determine 
(after we added all properties)
              * if we can keep the property as an operation or if we will need 
to copy the value in an attribute.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
index c614215ed9..cc00434d9d 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
@@ -174,7 +174,7 @@ final class ConvertFunction<R,S,V> extends 
UnaryFunction<R,S>
         if (fex == null) {
             return null;
         }
-        final FeatureProjectionBuilder.Item item = fex.expectedType(addTo);
+        final FeatureProjectionBuilder.Item item = 
addTo.addTemplateProperty(fex);
         item.replaceValueClass((c) -> getValueClass());
         return item;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
index 178a668113..07244202f4 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
@@ -97,7 +97,7 @@ final class GeometryFromFeature<G> extends 
GeometryConverter<Feature, G> {
     @Override
     public GeometryWrapper apply(final Feature input) {
         final GeometryWrapper wrapper = super.apply(input);
-        if (wrapper.getCoordinateReferenceSystem() == null) {
+        if (wrapper != null && wrapper.getCoordinateReferenceSystem() == null) 
{
             final CoordinateReferenceSystem crs = 
AttributeConvention.getCRSCharacteristic(input, propertyName);
             if (crs != null) {
                 wrapper.setCoordinateReferenceSystem(crs);
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
index 5be96dd66d..42d48014a7 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
@@ -28,6 +28,7 @@ import java.io.Serializable;
 import org.opengis.util.CodeList;
 import org.opengis.util.LocalName;
 import org.opengis.util.ScopedName;
+import org.opengis.referencing.IdentifiedObject;
 import org.apache.sis.math.FunctionProperty;
 import org.apache.sis.feature.DefaultAttributeType;
 import org.apache.sis.feature.internal.Resources;
@@ -35,6 +36,9 @@ import org.apache.sis.feature.privy.FeatureExpression;
 import org.apache.sis.filter.privy.WarningEvent;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.privy.ReferencingUtilities;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.iso.Names;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.collection.DefaultTreeTable;
@@ -328,6 +332,10 @@ public abstract class Node implements Serializable {
                 } else {
                     value = Vocabulary.format(Vocabulary.Keys.CycleOmitted);
                 }
+            } else if (child instanceof IdentifiedObject) {
+                final var object = (IdentifiedObject) child;
+                value = 
Classes.getShortName(ReferencingUtilities.getInterface(object))
+                        + "[“" + IdentifiedObjects.getDisplayName(object, 
null) + "”]";
             } else {
                 value = String.valueOf(child);
             }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
index b938087a8a..8d24f49ba2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
@@ -174,8 +174,8 @@ abstract class SpatialFunction<R> extends Node implements 
FeatureExpression<R,Ob
      * <ul class="verbose">
      *   <li>If the operation expects at least one geometric parameter and 
returns a geometry,
      *       then the characteristics of the first parameter (in particular 
the CRS) are copied.
-     *       The first parameter is used as a template for compliance with 
SQLMM specification.</li>
-     *   <li>Otherwise an attribute is created with the return value specified 
by the operation.</li>
+     *       The use of the first parameter is mandated by the 
<abbr>SQLMM</abbr> specification.</li>
+     *   <li>Otherwise, an attribute is created with the return value 
specified by the operation.</li>
      * </ul>
      *
      * @param  addTo  where to add the type of properties evaluated by this 
expression.
@@ -189,7 +189,7 @@ abstract class SpatialFunction<R> extends Node implements 
FeatureExpression<R,Ob
         if (operation.isGeometryInOut()) {
             final FeatureExpression<?,?> fex = 
FeatureExpression.castOrCopy(getParameters().get(0));
             if (fex != null) {
-                final FeatureProjectionBuilder.Item item = 
fex.expectedType(addTo);
+                final FeatureProjectionBuilder.Item item = 
addTo.addTemplateProperty(fex);
                 final boolean success = item.replaceValueClass((c) -> {
                     final Geometries<?> library = Geometries.factory(c);
                     return (library == null) ? null : 
operation.getReturnType(library);
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java
 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java
index 0ee3a909d0..927b7fe9d4 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java
@@ -42,16 +42,18 @@ import 
org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.setup.OptionKey;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.FeatureQuery;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.sql.SQLStore;
 import org.apache.sis.storage.sql.SQLStoreProvider;
 import org.apache.sis.storage.sql.SimpleFeatureStore;
 import org.apache.sis.storage.sql.ResourceDefinition;
+import org.apache.sis.storage.sql.feature.BinaryEncoding;
 import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.feature.privy.AttributeConvention;
 import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.io.stream.ChannelDataInput;
-import org.apache.sis.storage.sql.feature.BinaryEncoding;
 import org.apache.sis.geometry.wrapper.jts.JTS;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.util.Version;
@@ -158,6 +160,7 @@ public final class PostgresTest extends TestCase {
                 testAllFeatures(resource);
                 testFilteredFeatures(resource, false);
                 testFilteredFeatures(resource, true);
+                testGeometryTransform(resource);
             }
         }
     }
@@ -263,6 +266,61 @@ public final class PostgresTest extends TestCase {
         }
     }
 
+    /**
+     * Tests the {@code ST_Transform} <abbr>SQLMM</abbr> operation.
+     *
+     * @param  resource  the set of all features.
+     */
+    private static void testGeometryTransform(final FeatureSet resource) 
throws Exception {
+        final var factory   = DefaultFilterFactory.forFeatures();
+        final var targetCRS = 
factory.literal(GeometryGetterTest.getExpectedCRS(4326));
+        final var geometry  = factory.function("ST_Transform", 
factory.property("geometry"), targetCRS);
+        final var alias     = factory.function("ST_Transform", 
factory.property(AttributeConvention.GEOMETRY), targetCRS);
+        final var query     = new FeatureQuery();
+        query.setProjection(new 
FeatureQuery.NamedExpression(factory.property("filename")),
+                            new FeatureQuery.NamedExpression(geometry, 
"transformed"),
+                            new FeatureQuery.NamedExpression(alias),
+                            new 
FeatureQuery.NamedExpression(factory.property("image")));
+        final FeatureSet subset = resource.subset(query);
+        assertNull(getCRSCharacteristic(resource, "geometry"), "Expected no 
CRS because it is not the same for all rows.");
+        assertNull(getCRSCharacteristic(resource, 
AttributeConvention.GEOMETRY));
+        assertEquals(targetCRS.getValue(), getCRSCharacteristic(subset, 
"transformed"));
+        assertEquals(targetCRS.getValue(), getCRSCharacteristic(subset, 
AttributeConvention.GEOMETRY));
+        subset.features(false).forEach(PostgresTest::validateTransformed);
+    }
+
+    /**
+     * Invoked for each feature instances which is expected to have been 
transformed to WGS84.
+     */
+    private static void validateTransformed(final Feature feature) {
+        final Geometry geometry;
+        switch (feature.getPropertyValue("filename").toString()) {
+            case "point-prj": {
+                final var p = (Point) feature.getPropertyValue("transformed");
+                assertEquals(1.79663056824E-5, p.getX(), 1E-14);
+                assertEquals(2.71310843105E-5, p.getY(), 1E-14);
+                geometry = p;
+                break;
+            }
+            case "polygon-prj": {
+                geometry = (Geometry) feature.getPropertyValue("transformed");
+                final var envelope = geometry.getEnvelopeInternal();
+                // Tolerance is specified even if zero in order to ignore the 
sign of zero.
+                assertEquals(0, envelope.getMinX(), 0);
+                assertEquals(0, envelope.getMinY(), 0);
+                assertEquals(8.983152841195214E-6, envelope.getMaxX(), 1E-14);
+                assertEquals(9.043694770179478E-6, envelope.getMaxY(), 1E-14);
+                break;
+            }
+            default: {
+                validate(feature);
+                return;
+            }
+        }
+        verifySRID(4326, geometry);
+        assertEquals(geometry, 
feature.getPropertyValue(AttributeConvention.GEOMETRY));
+    }
+
     /**
      * Invoked for each feature instances for performing some checks on the 
feature.
      * This method performs only a superficial verification of geometries.
@@ -300,6 +358,18 @@ public final class PostgresTest extends TestCase {
             case "multi-polygon": geomSRID = 4326; break;
             default: throw new AssertionError(filename);
         }
+        assertNull(raster);
+        verifySRID(geomSRID, geometry);
+        assertEquals(geometry, 
feature.getPropertyValue(AttributeConvention.GEOMETRY));
+    }
+
+    /**
+     * Asserts that the given geometry as the expected <abbr>CRS</abbr>.
+     *
+     * @param geomSRID  the expected reference system identifier.
+     * @param geometry  the geometry to validate.
+     */
+    private static void verifySRID(final int geomSRID, final Geometry 
geometry) {
         try {
             final CoordinateReferenceSystem expected = 
GeometryGetterTest.getExpectedCRS(geomSRID);
             final CoordinateReferenceSystem actual = 
JTS.getCoordinateReferenceSystem(geometry);
@@ -314,6 +384,15 @@ public final class PostgresTest extends TestCase {
         } catch (FactoryException e) {
             throw new AssertionError(e);
         }
-        assertNull(raster);
+    }
+
+    /**
+     * Returns the {@code crs} characteristic of the geometry column of the 
given resource.
+     */
+    private static CoordinateReferenceSystem getCRSCharacteristic(final 
FeatureSet resource, final String property)
+            throws DataStoreException
+    {
+        final var type = resource.getType();
+        return AttributeConvention.getCRSCharacteristic(type, 
type.getProperty(property));
     }
 }

Reply via email to