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 <martin.desruisse...@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()); + } }