You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/04/28 13:31:29 UTC
[sis] 01/01: Merge remote-tracking branch 'origin/feat/computed-fields' into geoapi-4.0.
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 511ec7b89fc8dc1b94dfd9158282b5281d67e988
Merge: f6509802be 3704683e32
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Apr 28 15:31:07 2023 +0200
Merge remote-tracking branch 'origin/feat/computed-fields' into geoapi-4.0.
.../apache/sis/feature/ExpressionOperation.java | 227 +++++++++++++++++++++
.../org/apache/sis/feature/FeatureOperations.java | 51 ++++-
.../java/org/apache/sis/feature/LinkOperation.java | 2 +-
.../sis/feature/builder/AttributeTypeBuilder.java | 1 +
.../sis/internal/feature/FeatureExpression.java | 14 ++
.../java/org/apache/sis/storage/FeatureQuery.java | 135 ++++++++++--
.../org/apache/sis/storage/FeatureQueryTest.java | 63 +++++-
7 files changed, 473 insertions(+), 20 deletions(-)
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
index 0000000000,94349dec0c..78ecc4b7ad
mode 000000,100644..100644
--- 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
@@@ -1,0 -1,146 +1,227 @@@
+ /*
+ * 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.Map;
+ 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 java.util.HashSet;
++import java.util.Collection;
++import java.util.function.Function;
++import org.opengis.util.CodeList;
++import org.opengis.parameter.ParameterValueGroup;
++import org.opengis.parameter.ParameterDescriptorGroup;
+ import org.apache.sis.internal.feature.FeatureUtilities;
+ import org.apache.sis.internal.filter.FunctionNames;
+ import org.apache.sis.internal.filter.Visitor;
++
++// Branch-dependent imports
++import org.opengis.feature.Feature;
++import org.opengis.feature.Property;
+ 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.Expression;
+ 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.
++ * A feature property which is an operation implemented by a filter expression.
++ * This operation computes expression results from given feature instances only,
++ * there is no parameters.
+ *
- * @author Johann Sorel (Geomatys)
++ * @author Johann Sorel (Geomatys)
++ * @version 1.4
++ * @since 1.4
+ */
-public final class ExpressionOperation<V> extends AbstractOperation {
++final class ExpressionOperation<V> extends AbstractOperation {
++ /**
++ * For cross-version compatibility.
++ */
++ private static final long serialVersionUID = 5411697964136428848L;
++
++ /**
++ * The parameter descriptor for the "Expression" operation, which does not take any parameter.
++ */
++ private static final ParameterDescriptorGroup PARAMETERS = FeatureUtilities.parameters("Expression");
+
+ /**
- * The parameter descriptor for the "virtual" operation, which does not take any parameter.
++ * The expression on which to delegate the execution of this operation.
+ */
- private static final ParameterDescriptorGroup EMPTY_PARAMS = FeatureUtilities.parameters("Virtual");
++ @SuppressWarnings("serial") // Not statically typed as serializable.
++ private final Function<? super Feature, ? extends V> expression;
+
- private static final ListPropertyVisitor VISITOR = new ListPropertyVisitor();
++ /**
++ * The type of result of evaluating the expression.
++ */
++ @SuppressWarnings("serial") // Apache SIS implementations are serializable.
++ private final AttributeType<? super V> result;
+
- private final FeatureExpression<Feature,V> expression;
- private final AttributeType<V> type;
++ /**
++ * The name of all feature properties that are known to be read by the expression.
++ * This is determined by execution of {@link #VISITOR} on the {@linkplain #expression}.
++ * This set may be incomplete if some properties are read otherwise than by {@link ValueReference}.
++ */
++ @SuppressWarnings("serial") // Set.of(…) implementations are serializable.
+ private final Set<String> dependencies;
+
- public ExpressionOperation(GenericName name, FeatureExpression<Feature,V> expression, FeatureType featureType) {
- super(Collections.singletonMap(DefaultAttributeType.NAME_KEY, name));
++ /**
++ * Creates a new operation which will delegate execution to the given expression.
++ *
++ * @param identification the name of the operation, together with optional information.
++ * @param expression the expression to evaluate on feature instances.
++ * @param result type of values computed by the expression.
++ */
++ ExpressionOperation(final Map<String,?> identification,
++ final Function<? super Feature, ? extends V> expression,
++ final AttributeType<? super V> result)
++ {
++ super(identification);
+ 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;
++ this.result = result;
++ if (expression instanceof Expression<?,?>) {
++ dependencies = DependencyFinder.search((Expression<Object,?>) expression);
++ } else {
++ dependencies = Set.of();
++ }
+ }
+
++ /**
++ * Returns a description of the input parameters.
++ */
+ @Override
+ public ParameterDescriptorGroup getParameters() {
- return EMPTY_PARAMS;
++ return PARAMETERS;
+ }
+
++ /**
++ * Returns the expected result type.
++ */
+ @Override
+ public IdentifiedType getResult() {
- return type;
++ return result;
+ }
+
++ /**
++ * Returns the names of feature properties that this operation needs for performing its task.
++ * This set may be incomplete if some properties are read otherwise than by {@link ValueReference}.
++ */
+ @Override
++ @SuppressWarnings("ReturnOfCollectionOrArrayField") // Because the set is unmodifiable.
+ public Set<String> getDependencies() {
+ return dependencies;
+ }
+
++ /**
++ * Returns the value computed by the expression for the given feature instance.
++ *
++ * @param feature the feature to evaluate with the expression.
++ * @param parameters ignored (can be {@code null}).
++ * @return the computed property from the given feature.
++ */
+ @Override
- public Property apply(Feature feature, ParameterValueGroup parameters) {
- final Attribute<V> att = type.newInstance();
- att.setValue(expression.apply(feature));
- return att;
++ public Property apply(final Feature feature, ParameterValueGroup parameters) {
++ final Attribute<? super V> instance = result.newInstance();
++ instance.setValue(expression.apply(feature));
++ return instance;
+ }
+
- private static final class ListPropertyVisitor extends Visitor<Object,Collection<String>> {
++ /**
++ * Computes a hash-code value for this operation.
++ */
++ @Override
++ public int hashCode() {
++ return super.hashCode() + expression.hashCode();
++ }
++
++ /**
++ * Compares this operation with the given object for equality.
++ */
++ @Override
++ public boolean equals(final Object obj) {
++ /*
++ * `this.result` is compared (indirectly) by the super class.
++ * `this.dependencies` does not need to be compared because it is derived from `expression`.
++ */
++ return super.equals(obj) && expression.equals(((ExpressionOperation) obj).expression);
++ }
+
- protected ListPropertyVisitor() {
- setLogicalHandlers((f, names) -> {
- final LogicalOperator<Object> filter = (LogicalOperator<Object>) f;
++ /**
++ * An expression visitor for finding all dependencies of a given expression.
++ * The dependencies are feature properties read by {@link ValueReference} nodes.
++ *
++ * @todo The first parameterized type should be {@code Feature} instead of {@code Object}.
++ */
++ private static final class DependencyFinder extends Visitor<Object, Collection<String>> {
++ /**
++ * The unique instance.
++ */
++ private static final DependencyFinder VISITOR = new DependencyFinder();
++
++ /**
++ * Returns all dependencies read by a {@link ValueReference} node.
++ *
++ * @param expression the expression for which to get dependencies.
++ * @return all dependencies recognized by this method.
++ */
++ static Set<String> search(final Expression<Object,?> expression) {
++ final Set<String> dependencies = new HashSet<>();
++ VISITOR.visit(expression, dependencies);
++ return Set.copyOf(dependencies);
++ }
++
++ /**
++ * Constructor for the unique instance.
++ */
++ private DependencyFinder() {
++ setLogicalHandlers((f, dependencies) -> {
++ final var filter = (LogicalOperator<Object>) f;
+ for (Filter<Object> child : filter.getOperands()) {
- visit(child, names);
++ visit(child, dependencies);
+ }
+ });
- 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;
++ setExpressionHandler(FunctionNames.ValueReference, (e, dependencies) -> {
++ final var expression = (ValueReference<Object,?>) e;
+ final String propName = expression.getXPath();
+ if (!propName.trim().isEmpty()) {
- names.add(propName);
++ dependencies.add(propName);
+ }
+ });
+ }
+
++ /**
++ * Fallback for all filters not explicitly handled by the setting applied in the constructor.
++ */
+ @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);
++ protected void typeNotFound(final CodeList<?> type, final Filter<Object> filter, final Collection<String> dependencies) {
++ for (final Expression<Object,?> f : filter.getExpressions()) {
++ visit(f, dependencies);
+ }
+ }
+
++ /**
++ * Fallback for all expressions not explicitly handled by the setting applied in the constructor.
++ */
+ @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);
++ protected void typeNotFound(final String type, final Expression<Object,?> expression, final Collection<String> dependencies) {
++ for (final Expression<Object,?> p : expression.getParameters()) {
++ visit(p, dependencies);
+ }
+ }
+ }
+ }
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
index c4ddb1c2b8,c4ddb1c2b8..ad4c132716
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
@@@ -17,6 -17,6 +17,7 @@@
package org.apache.sis.feature;
import java.util.Map;
++import java.util.function.Function;
import org.opengis.util.GenericName;
import org.opengis.util.FactoryException;
import org.opengis.util.InternationalString;
@@@ -28,9 -28,9 +29,12 @@@ import org.apache.sis.util.collection.W
import org.apache.sis.util.resources.Errors;
// Branch-dependent imports
++import org.opengis.feature.Feature;
import org.opengis.feature.Operation;
import org.opengis.feature.PropertyType;
++import org.opengis.feature.AttributeType;
import org.opengis.feature.FeatureAssociationRole;
++import org.opengis.filter.Expression;
/**
@@@ -107,7 -107,7 +111,7 @@@
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.0
++ * @version 1.4
* @since 0.7
*/
public final class FeatureOperations extends Static {
@@@ -260,4 -260,4 +264,49 @@@
ArgumentChecks.ensureNonNull("geometryAttributes", geometryAttributes);
return POOL.unique(new EnvelopeOperation(identification, crs, geometryAttributes));
}
++
++ /**
++ * Creates an operation which delegates the computation to a given expression.
++ * The {@code expression} argument should generally be an instance of
++ * {@link org.opengis.filter.Expression},
++ * but more generic functions are accepted as well.
++ *
++ * @param <V> the type of values computed by the expression and assigned to the feature property.
++ * @param identification the name of the operation, together with optional information.
++ * @param expression the expression to evaluate on feature instances.
++ * @param result type of values computed by the expression and assigned to the feature property.
++ * @return a feature operation which computes its values using the given expression.
++ *
++ * @since 1.4
++ */
++ public static <V> Operation expression(final Map<String,?> identification,
++ final Function<? super Feature, ? extends V> expression,
++ final AttributeType<? super V> result)
++ {
++ ArgumentChecks.ensureNonNull("expression", expression);
++ return POOL.unique(new ExpressionOperation<>(identification, expression, result));
++ }
++
++ /**
++ * Creates an operation which delegates the computation to a given expression producing values of unknown type.
++ * This method can be used as an alternative to {@link #expression expression(…)} when the constraint on the
++ * parameterized type {@code <V>} between {@code expression} and {@code result} can not be enforced at compile time.
++ * This method casts or converts the expression to the expected type by a call to
++ * {@link Expression#toValueType(Class)}.
++ *
++ * @param <V> the type of values computed by the expression and assigned to the feature property.
++ * @param identification the name of the operation, together with optional information.
++ * @param expression the expression to evaluate on feature instances.
++ * @param result type of values computed by the expression and assigned to the feature property.
++ * @return a feature operation which computes its values using the given expression.
++ * @throws ClassCastException if the result type is not a target type supported by the expression.
++ *
++ * @since 1.4
++ */
++ public static <V> Operation expressionToResult(final Map<String,?> identification,
++ final Expression<? super Feature, ?> expression,
++ final AttributeType<V> result)
++ {
++ return expression(identification, expression.toValueType(result.getValueClass()), result);
++ }
}
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
index c83f847b61,c83f847b61..e974d358da
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
@@@ -133,7 -133,7 +133,7 @@@ final class LinkOperation extends Abstr
*/
@Override
public boolean equals(final Object obj) {
-- // 'this.result' is compared (indirectly) by the super class.
++ // `this.result` is compared (indirectly) by the super class.
return super.equals(obj) && referentName.equals(((LinkOperation) obj).referentName);
}
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
index 8a1f10aa71,8a1f10aa71..15acc606d4
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
@@@ -177,6 -177,6 +177,7 @@@ public final class AttributeTypeBuilder
* Sets the {@code AttributeType} name as a generic name.
* If another name was defined before this method call, that previous value will be discarded.
*
++ * @param name the attribute name (cannot be {@code null}).
* @return {@code this} for allowing method calls chaining.
*/
@Override
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
index cc81863b73,cc81863b73..86b379a201
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
@@@ -25,6 -25,6 +25,7 @@@ import org.apache.sis.filter.Optimizati
import org.apache.sis.filter.DefaultFilterFactory;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
import org.apache.sis.feature.builder.PropertyTypeBuilder;
++import org.apache.sis.feature.builder.AttributeTypeBuilder;
/**
@@@ -47,6 -47,6 +48,12 @@@ public interface FeatureExpression<R,V
/**
* Returns the type of values computed by this expression, or {@code Object.class} if unknown.
*
++ * <h4>Note on type safety</h4>
++ * The parameterized type should be {@code <? extends V>} because some implementations get this
++ * information by a call to {@code value.getClass()}. But it should also be {@code <? super V>}
++ * for supporting the {@code Object.class} return value. Those contradictory requirements force
++ * us to use {@code <?>}.
++ *
* @return the type of values computed by this expression.
*/
default Class<?> getValueClass() {
@@@ -59,6 -59,6 +66,9 @@@
* {@link AttributeType} or a {@link org.opengis.feature.FeatureAssociationRole}
* but not an {@link org.opengis.feature.Operation}.
*
++ * <p>If this method returns an instance of {@link AttributeTypeBuilder}, then its parameterized
++ * type should be the same {@code <V>} than this {@code FeatureExpression}.</p>
++ *
* @param valueType the type of features to be evaluated by the given expression.
* @param addTo where to add the type of properties evaluated by this expression.
* @return builder of the added property, or {@code null} if this method cannot add a property.
@@@ -81,6 -81,6 +91,10 @@@
* It is caller's responsibility to verify if this method returns {@code null} and to throw an exception in such case.
* We leave that responsibility to the caller because (s)he may be able to provide better error messages.
*
++ * <h4>Note on type safety</h4>
++ * This method does not use parameterized types because of the assumption on {@link ValueReference}.
++ * As of Apache SIS 1.3, we have no way to check if {@code <R>} is for feature instances.
++ *
* @param candidate the expression to cast or copy. Can be null.
* @return the given expression as a feature expression, or {@code null} if it cannot be casted or converted.
*/
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
index e7409c28ba,ba751928ea..3fdbb0720a
--- 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
@@@ -23,13 -23,14 +23,17 @@@ import java.util.Map
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.OptionalLong;
++import java.util.function.Function;
import java.io.Serializable;
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.AbstractOperation;
++import org.apache.sis.feature.FeatureOperations;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
import org.apache.sis.feature.builder.PropertyTypeBuilder;
++import org.apache.sis.feature.builder.AttributeTypeBuilder;
import org.apache.sis.internal.feature.AttributeConvention;
import org.apache.sis.internal.feature.FeatureExpression;
import org.apache.sis.internal.filter.SortByComparator;
@@@ -45,6 -46,6 +49,9 @@@ import org.apache.sis.util.resources.Vo
// Branch-dependent imports
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
++import org.opengis.feature.Attribute;
++import org.opengis.feature.AttributeType;
++import org.opengis.feature.Operation;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.Filter;
import org.opengis.filter.Expression;
@@@ -65,7 -66,7 +72,7 @@@ import org.opengis.filter.InvalidFilter
* <ul>
* <li>A <cite>selection</cite> is a filter choosing the features instances to include in the subset.
* In relational databases, a feature instances are mapped to table rows.</li>
-- * <li>A <cite>projection</cite> (not to be confused with map projection) is the set of feature property to keep.
++ * <li>A <cite>projection</cite> (not to be confused with map projection) is the set of feature properties to keep.
* In relational databases, feature properties are mapped to table columns.</li>
* </ul>
*
@@@ -75,7 -76,7 +82,7 @@@
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.2
++ * @version 1.4
* @since 1.1
*/
public class FeatureQuery extends Query implements Cloneable, Serializable {
@@@ -400,19 -401,19 +407,76 @@@
return linearResolution;
}
++ /**
++ * Whether a property evaluated by a query is computed on the fly or stored.
++ * By default, an expression is evaluated only once for each feature instance,
++ * then the result is stored as a feature {@link Attribute} value.
++ * But the same expression can also be wrapped in a feature {@link Operation}
++ * and evaluated every times that the value is requested.
++ *
++ * <h2>Analogy with relational databases</h2>
++ * The terminology used in this enumeration is close to the one used in relational database.
++ * A <cite>projection</cite> is the set of feature properties to keep in the query results.
++ * The projection may contain <cite>generated columns</cite>, which are specified in SQL by
++ * {@code SQL GENERATED ALWAYS} statement, optionally with {@code STORED} or {@code VIRTUAL}
++ * modifier.
++ *
++ * @version 1.4
++ * @since 1.4
++ */
++ public enum ProjectionType {
++ /**
++ * The expression is evaluated exactly once when a feature instance is created,
++ * and the result is stored as a feature attribute.
++ * The feature property type will be {@link Attribute} and its value will be modifiable.
++ * This is the default projection type.
++ */
++ STORED,
++
++ /**
++ * The expression is evaluated every times that the property value is requested.
++ * The feature property type will be {@link Operation}.
++ * This projection type may be preferable to {@link #STORED} in the following circumstances:
++ *
++ * <ul>
++ * <li>The expression may produce different results every times that it is evaluated.</li>
++ * <li>The feature property should be a {@linkplain FeatureOperations#link link} to another attribute.</li>
++ * <li>Potentially expensive computation should be deferred until first needed.</li>
++ * <li>Computation result should not be stored in order to reduce memory usage.</li>
++ * </ul>
++ *
++ * @see FeatureOperations#expression(Map, Function, AttributeType)
++ */
++ VIRTUAL
++
++ /*
++ * Examples of other enumeration values that we may add in the future:
++ * GENERATED for meaning "STORED but read-only", CACHED for lazy computation.
++ */
++ }
++
/**
* An expression to be retrieved by a {@code Query}, together with the name to assign to it.
++ * {@code NamedExpression} specifies also if the expression should be evaluated exactly once
++ * and its value stored, or evaluated every times that the value is requested.
++ *
++ * <h2>Analogy with relational databases</h2>
++ * A {@code NamedExpression} instance can be understood as the definition of a column in a SQL database table.
* In relational database terminology, subset of columns is called <cite>projection</cite>.
++ * A projection is specified by a SQL {@code SELECT} statement, which maps to {@code NamedExpression} as below:
++ *
++ * <p>{@code SELECT} {@link #expression} {@code AS} {@link #alias}</p>
++ *
* Columns can be given to the {@link FeatureQuery#setProjection(NamedExpression[])} method.
*
-- * @version 1.2
++ * @version 1.4
* @since 1.1
*/
public static class NamedExpression implements Serializable {
/**
* For cross-version compatibility.
*/
-- private static final long serialVersionUID = -6919525113513842514L;
++ private static final long serialVersionUID = 4547204390645035145L;
/**
* The literal, value reference or more complex expression to be retrieved by a {@code Query}.
@@@ -428,30 -429,39 +492,36 @@@
public final GenericName alias;
/**
- * Creates a new column with the given expression and no name.
- * A virtual expression will only exist as an Operation.
- * Those are commonly called 'computed fields' and equivalant of
- * SQL GENERATED ALWAYS keyword for columns.
++ * Whether the expression result should be stored or evaluated every times that it is requested.
++ * A stored value will exist as a feature {@link Attribute}, while a virtual value will exist as
++ * a feature {@link Operation}. The latter are commonly called "computed fields" and are equivalent
++ * to SQL {@code GENERATED ALWAYS} keyword for columns.
++ *
++ * @since 1.4
+ */
- public final boolean virtual;
++ public final ProjectionType type;
+
+ /**
- * Creates a new column with the given expression and no name.
++ * Creates a new stored column with the given expression and no name.
*
* @param expression the literal, value reference or expression to be retrieved by a {@code Query}.
*/
public NamedExpression(final Expression<? super Feature, ?> expression) {
-- ArgumentChecks.ensureNonNull("expression", expression);
-- this.expression = expression;
-- this.alias = null;
- this.virtual = false;
++ this(expression, (GenericName) null);
}
/**
- * 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.
++ * Creates a new stored 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.
*/
public NamedExpression(final Expression<? super Feature, ?> expression, final GenericName alias) {
-- ArgumentChecks.ensureNonNull("expression", expression);
-- this.expression = expression;
-- this.alias = alias;
- this.virtual = false;
++ this(expression, alias, ProjectionType.STORED);
}
/**
- * 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.
++ * Creates a new stored 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 +531,24 @@@
ArgumentChecks.ensureNonNull("expression", expression);
this.expression = expression;
this.alias = (alias != null) ? Names.createLocalName(null, null, alias) : null;
- this.virtual = false;
++ this.type = ProjectionType.STORED;
+ }
+
+ /**
- * Creates a new column with the given expression and the given name.
++ * Creates a new column with the given expression, the given name and the given projection type.
+ *
+ * @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.
++ * @param type whether to create a feature {@link Attribute} or a feature {@link Operation}.
++ *
++ * @since 1.4
+ */
- public NamedExpression(final Expression<? super Feature, ?> expression, final GenericName alias, boolean virtual) {
++ public NamedExpression(final Expression<? super Feature, ?> expression, final GenericName alias, ProjectionType type) {
+ ArgumentChecks.ensureNonNull("expression", expression);
++ ArgumentChecks.ensureNonNull("type", type);
+ this.expression = expression;
+ this.alias = alias;
- this.virtual = virtual;
++ this.type = type;
}
/**
@@@ -470,7 -495,7 +558,7 @@@
*/
@Override
public int hashCode() {
-- return 37 * expression.hashCode() + Objects.hashCode(alias);
++ return 37 * expression.hashCode() + Objects.hashCode(alias) + type.hashCode();
}
/**
@@@ -486,7 -511,7 +574,7 @@@
}
if (obj != null && getClass() == obj.getClass()) {
final NamedExpression other = (NamedExpression) obj;
-- return expression.equals(other.expression) && Objects.equals(alias, other.alias);
++ return expression.equals(other.expression) && Objects.equals(alias, other.alias) && type == other.type;
}
return false;
}
@@@ -593,19 -618,20 +681,20 @@@
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++) {
++ final NamedExpression item = projection[column];
/*
* For each property, get the expected type (mandatory) and its name (optional).
* A default name will be computed if no alias were explicitly given by user.
*/
-- GenericName name = projection[column].alias;
-- final Expression<?,?> expression = projection[column].expression;
++ final Expression<? super Feature,?> expression = item.expression;
final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(expression);
final PropertyTypeBuilder resultType;
if (fex == null || (resultType = fex.expectedType(valueType, ftb)) == null) {
throw new InvalidFilterValueException(Resources.format(Resources.Keys.InvalidExpression_2,
expression.getFunctionName().toInternationalString(), column));
}
++ GenericName name = item.alias;
if (name == null) {
/*
* Build a list of aliases declared by the user, for making sure that we do not collide with them.
@@@ -649,8 -675,26 +738,20 @@@
name = Names.createLocalName(null, null, text);
}
resultType.setName(name);
- columnNames[column] = name;
- }
-
- 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));
++ /*
++ * If the attribute that we just added should be virtual,
++ * replace the attribute by an operation.
++ */
++ if (item.type == ProjectionType.VIRTUAL && resultType instanceof AttributeTypeBuilder<?>) {
++ final var ab = (AttributeTypeBuilder<?>) resultType;
++ final AttributeType<?> storedType = ab.build();
++ if (ftb.properties().remove(resultType)) {
++ final var properties = Map.of(AbstractOperation.NAME_KEY, name);
++ ftb.addProperty(FeatureOperations.expressionToResult(properties, expression, storedType));
++ }
+ }
}
- featureType = ftb.build();
-
- return featureType;
+ return ftb.build();
}
/**
diff --cc storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
index 0286def28a,144c71e22d..ca2dfb0321
--- 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
@@@ -34,6 -35,7 +35,8 @@@ 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.Expression;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.MatchAction;
@@@ -47,7 -49,7 +50,7 @@@ import org.opengis.filter.SortProperty
* @author Johann Sorel (Geomatys)
* @author Alexis Manin (Geomatys)
* @author Martin Desruisseaux (Geomatys)
-- * @version 1.3
++ * @version 1.4
* @since 1.0
*/
public final class FeatureQueryTest extends TestCase {
@@@ -319,4 -321,53 +322,62 @@@
assertEquals("value1", 2, instance.getPropertyValue("value1"));
assertEquals("value3", 25, instance.getPropertyValue("value3"));
}
+
++ /**
++ * Shortcut for creating expression for a projection computed on-the-fly.
++ */
++ private static FeatureQuery.NamedExpression virtualProjection(final Expression<? super Feature, ?> expression, final String alias) {
++ return new FeatureQuery.NamedExpression(expression, Names.createLocalName(null, null, alias), FeatureQuery.ProjectionType.VIRTUAL);
++ }
++
+ /**
+ * 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));
++ query.setProjection(
++ new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) null),
++ virtualProjection(ff.property("value1", Integer.class), "renamed1"),
++ virtualProjection(ff.literal("a literal"), "computed"));
+
+ // 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.
++ * Verifies that 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));
++ virtualProjection(ff.property("valueMissing", Integer.class), "renamed1"));
+
- assertThrows(DataStoreContentException.class, () -> executeAndGetFirst());
++ DataStoreContentException ex = assertThrows(DataStoreContentException.class, this::executeAndGetFirst);
++ assertNotNull(ex.getMessage());
+ }
}