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