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

jsorel pushed a commit to branch feat/computed-fields
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 3704683e32130c3f2aac10c75a41fb78187f7a84
Author: jsorel <johann.so...@geomatys.com>
AuthorDate: Mon Mar 13 14:39:06 2023 +0100

    feat(FeatureQuery): add support for computed fields in query
---
 .../apache/sis/feature/ExpressionOperation.java    | 146 +++++++++++++++++++++
 .../java/org/apache/sis/storage/FeatureQuery.java  |  50 ++++++-
 .../org/apache/sis/storage/FeatureQueryTest.java   |  51 +++++++
 3 files changed, 244 insertions(+), 3 deletions(-)

diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
 
b/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
new file mode 100644
index 0000000000..94349dec0c
--- /dev/null
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
@@ -0,0 +1,146 @@
+/*
+ * 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.feature;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.feature.builder.PropertyTypeBuilder;
+import org.apache.sis.internal.feature.FeatureExpression;
+import org.apache.sis.internal.feature.FeatureUtilities;
+import org.apache.sis.internal.filter.FunctionNames;
+import org.apache.sis.internal.filter.Visitor;
+import org.opengis.feature.Attribute;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Property;
+import org.opengis.filter.BetweenComparisonOperator;
+import org.opengis.filter.ComparisonOperatorName;
+import org.opengis.filter.Expression;
+import org.opengis.filter.Filter;
+import org.opengis.filter.LikeOperator;
+import org.opengis.filter.LogicalOperator;
+import org.opengis.filter.ValueReference;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.util.CodeList;
+import org.opengis.util.GenericName;
+
+/**
+ * An operation computing the result of expression on current feature.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ExpressionOperation<V> extends AbstractOperation {
+
+    /**
+     * The parameter descriptor for the "virtual" operation, which does not 
take any parameter.
+     */
+    private static final ParameterDescriptorGroup EMPTY_PARAMS = 
FeatureUtilities.parameters("Virtual");
+
+    private static final ListPropertyVisitor VISITOR = new 
ListPropertyVisitor();
+
+    private final FeatureExpression<Feature,V> expression;
+    private final AttributeType<V> type;
+    private final Set<String> dependencies;
+
+    public ExpressionOperation(GenericName name, FeatureExpression<Feature,V> 
expression, FeatureType featureType) {
+        super(Collections.singletonMap(DefaultAttributeType.NAME_KEY, name));
+        this.expression = expression;
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        PropertyTypeBuilder expectedType = 
expression.expectedType(featureType, ftb);
+        expectedType.setName(name);
+        type = (AttributeType<V>) expectedType.build();
+
+        final Set<String> dependencies = new HashSet<>();
+        VISITOR.visit((Expression) expression, dependencies);
+        this.dependencies = Collections.unmodifiableSet(dependencies);
+    }
+
+    public FeatureExpression<Feature, V> getExpression() {
+        return expression;
+    }
+
+    @Override
+    public ParameterDescriptorGroup getParameters() {
+        return EMPTY_PARAMS;
+    }
+
+    @Override
+    public IdentifiedType getResult() {
+        return type;
+    }
+
+    @Override
+    public Set<String> getDependencies() {
+        return dependencies;
+    }
+
+    @Override
+    public Property apply(Feature feature, ParameterValueGroup parameters) {
+        final Attribute<V> att = type.newInstance();
+        att.setValue(expression.apply(feature));
+        return att;
+    }
+
+    private static final class ListPropertyVisitor extends 
Visitor<Object,Collection<String>> {
+
+        protected ListPropertyVisitor() {
+            setLogicalHandlers((f, names) -> {
+                final LogicalOperator<Object> filter = 
(LogicalOperator<Object>) f;
+                for (Filter<Object> child : filter.getOperands()) {
+                    visit(child, names);
+                }
+            });
+            
setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_BETWEEN),
 (f, names) -> {
+                final BetweenComparisonOperator<Object> filter = 
(BetweenComparisonOperator<Object>) f;
+                visit(filter.getExpression(),    names);
+                visit(filter.getLowerBoundary(), names);
+                visit(filter.getUpperBoundary(), names);
+            });
+            
setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_LIKE),
 (f, names) -> {
+                final LikeOperator<Object> filter = (LikeOperator<Object>) f;
+                visit(filter.getExpressions().get(0), names);
+            });
+            setExpressionHandler(FunctionNames.ValueReference, (e, names) -> {
+                final ValueReference<Object,?> expression = 
(ValueReference<Object,?>) e;
+                final String propName = expression.getXPath();
+                if (!propName.trim().isEmpty()) {
+                    names.add(propName);
+                }
+            });
+        }
+
+        @Override
+        protected void typeNotFound(final CodeList<?> type, final 
Filter<Object> filter, final Collection<String> names) {
+            for (final Expression<? super Object, ?> f : 
filter.getExpressions()) {
+                visit(f, names);
+            }
+        }
+
+        @Override
+        protected void typeNotFound(final String type, final 
Expression<Object, ?> expression, final Collection<String> names) {
+            for (final Expression<? super Object, ?> p : 
expression.getParameters()) {
+                visit(p, names);
+            }
+        }
+    }
+}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
index e7409c28ba..ba751928ea 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
@@ -28,6 +28,7 @@ import javax.measure.Quantity;
 import javax.measure.quantity.Length;
 import org.opengis.util.GenericName;
 import org.opengis.geometry.Envelope;
+import org.apache.sis.feature.ExpressionOperation;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.feature.builder.PropertyTypeBuilder;
 import org.apache.sis.internal.feature.AttributeConvention;
@@ -427,6 +428,13 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
         @SuppressWarnings("serial")                 // Most SIS 
implementations are serializable.
         public final GenericName alias;
 
+        /**
+         * A virtual expression will only exist as an Operation.
+         * Those are commonly called 'computed fields' and equivalant of
+         * SQL GENERATED ALWAYS keyword for columns.
+         */
+        public final boolean virtual;
+
         /**
          * Creates a new column with the given expression and no name.
          *
@@ -436,10 +444,11 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
             ArgumentChecks.ensureNonNull("expression", expression);
             this.expression = expression;
             this.alias = null;
+            this.virtual = false;
         }
 
         /**
-         * Creates a new column with the given expression and the given name.
+         * Creates a new persistant column with the given expression and the 
given name.
          *
          * @param expression  the literal, value reference or expression to be 
retrieved by a {@code Query}.
          * @param alias       the name to assign to the expression result, or 
{@code null} if unspecified.
@@ -448,10 +457,11 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
             ArgumentChecks.ensureNonNull("expression", expression);
             this.expression = expression;
             this.alias = alias;
+            this.virtual = false;
         }
 
         /**
-         * Creates a new column with the given expression and the given name.
+         * Creates a new persistant column with the given expression and the 
given name.
          * This constructor creates a {@link org.opengis.util.LocalName} from 
the given string.
          *
          * @param expression  the literal, value reference or expression to be 
retrieved by a {@code Query}.
@@ -461,6 +471,21 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
             ArgumentChecks.ensureNonNull("expression", expression);
             this.expression = expression;
             this.alias = (alias != null) ? Names.createLocalName(null, null, 
alias) : null;
+            this.virtual = false;
+        }
+
+        /**
+         * Creates a new column with the given expression and the given name.
+         *
+         * @param expression  the literal, value reference or expression to be 
retrieved by a {@code Query}.
+         * @param alias       the name to assign to the expression result, or 
{@code null} if unspecified.
+         * @param virtual     true to create a computed field, an Operation.
+         */
+        public NamedExpression(final Expression<? super Feature, ?> 
expression, final GenericName alias, boolean virtual) {
+            ArgumentChecks.ensureNonNull("expression", expression);
+            this.expression = expression;
+            this.alias = alias;
+            this.virtual = virtual;
         }
 
         /**
@@ -593,6 +618,7 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
         int unnamedNumber = 0;          // Sequential number for unnamed 
expressions.
         Set<String> names = null;       // Names already used, for avoiding 
collisions.
         final FeatureTypeBuilder ftb = new 
FeatureTypeBuilder().setName(valueType.getName());
+        final GenericName[] columnNames = new GenericName[projection.length];
         for (int column = 0; column < projection.length; column++) {
             /*
              * For each property, get the expected type (mandatory) and its 
name (optional).
@@ -649,8 +675,26 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
                 name = Names.createLocalName(null, null, text);
             }
             resultType.setName(name);
+            columnNames[column] = name;
         }
-        return ftb.build();
+
+        FeatureType featureType = ftb.build();
+        /*
+         * Build virtual fields.
+         * This operation must be done in a second loop because computed fields
+         * rely on the projected fields only.
+         */
+        for (int column = 0; column < projection.length; column++) {
+            if (projection[column].virtual) {
+                //make current property virtual
+                ftb.properties().remove(columnNames[column]);
+                final FeatureExpression<?,?> fex = 
FeatureExpression.castOrCopy(projection[column].expression);
+                ftb.addProperty(new ExpressionOperation(columnNames[column], 
fex, featureType));
+            }
+        }
+        featureType = ftb.build();
+
+        return featureType;
     }
 
     /**
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
index 0286def28a..144c71e22d 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
@@ -25,6 +25,7 @@ import org.apache.sis.internal.storage.MemoryFeatureSet;
 import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.TestCase;
+import org.apache.sis.util.iso.Names;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
@@ -34,6 +35,7 @@ import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
 import org.opengis.feature.AttributeType;
+import org.opengis.feature.Operation;
 import org.opengis.filter.Filter;
 import org.opengis.filter.FilterFactory;
 import org.opengis.filter.MatchAction;
@@ -319,4 +321,53 @@ public final class FeatureQueryTest extends TestCase {
         assertEquals("value1",  2, instance.getPropertyValue("value1"));
         assertEquals("value3", 25, instance.getPropertyValue("value3"));
     }
+
+    /**
+     * Verifies the effect of virtual projections.
+     *
+     * @throws DataStoreException if an error occurred while executing the 
query.
+     */
+    @Test
+    public void testVirtualProjection() throws DataStoreException {
+        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+        query.setProjection(new 
FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) 
null),
+                            new 
FeatureQuery.NamedExpression(ff.property("value1", Integer.class), 
Names.createLocalName(null, null, "renamed1"), true),
+                            new FeatureQuery.NamedExpression(ff.literal("a 
literal"), Names.createLocalName(null, null, "computed"), true));
+
+        // Check result type.
+        final Feature instance = executeAndGetFirst();
+        final FeatureType resultType = instance.getType();
+        assertEquals("Test", resultType.getName().toString());
+        assertEquals(3, resultType.getProperties(true).size());
+        final PropertyType pt1 = resultType.getProperty("value1");
+        final PropertyType pt2 = resultType.getProperty("renamed1");
+        final PropertyType pt3 = resultType.getProperty("computed");
+        assertTrue(pt1 instanceof AttributeType);
+        assertTrue(pt2 instanceof Operation);
+        assertTrue(pt3 instanceof Operation);
+        assertEquals(Integer.class, ((AttributeType) pt1).getValueClass());
+        assertTrue(((Operation) pt2).getResult() instanceof AttributeType);
+        assertTrue(((Operation) pt3).getResult() instanceof AttributeType);
+        assertEquals(Integer.class, ((AttributeType)((Operation) 
pt2).getResult()).getValueClass());
+        assertEquals(String.class,  ((AttributeType)((Operation) 
pt3).getResult()).getValueClass());
+
+        // Check feature instance.
+        assertEquals(3, instance.getPropertyValue("value1"));
+        assertEquals(3, instance.getPropertyValue("renamed1"));
+        assertEquals("a literal", instance.getPropertyValue("computed"));
+    }
+
+    /**
+     * Verifies a virtual projection on a missing field causes an exception.
+     *
+     * @throws DataStoreException if an error occurred while executing the 
query.
+     */
+    @Test
+    public void testIncorrectVirtualProjection() throws DataStoreException {
+        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+        query.setProjection(new 
FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) 
null),
+                            new 
FeatureQuery.NamedExpression(ff.property("valueMissing", Integer.class), 
Names.createLocalName(null, null, "renamed1"), true));
+
+        assertThrows(DataStoreContentException.class, () -> 
executeAndGetFirst());
+    }
 }

Reply via email to