This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new f1393024bc When some filters implemented in pure Java are replaced by
SQL expressions, remove the properties of `typeWithDependencies` that are not
longer needed. It can reduce the number of columns requested in the `SELECT`
statement.
f1393024bc is described below
commit f1393024bc65d62394c4abf663760ed937b99c69
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Nov 21 17:08:51 2025 +0100
When some filters implemented in pure Java are replaced by SQL expressions,
remove the properties of `typeWithDependencies` that are not longer needed.
It can reduce the number of columns requested in the `SELECT` statement.
---
.../apache/sis/feature/ExpressionOperation.java | 2 +-
.../org/apache/sis/feature/FeatureOperations.java | 55 +++++-
.../main/org/apache/sis/feature/Features.java | 15 +-
.../sis/feature/builder/FeatureTypeBuilder.java | 8 +-
.../feature/internal/shared/FeatureProjection.java | 208 ++++++++++++++++-----
.../internal/shared/FeatureProjectionTest.java | 79 ++++++--
.../org/apache/sis/storage/sql/feature/Column.java | 2 +-
.../sis/storage/sql/feature/FeatureStream.java | 7 +-
8 files changed, 299 insertions(+), 77 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
index c7f10889d8..a395f76219 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
@@ -68,7 +68,7 @@ final class ExpressionOperation<V> extends AbstractOperation {
* The type of result of evaluating the expression.
*/
@SuppressWarnings("serial") // Apache SIS
implementations are serializable.
- private final AttributeType<V> resultType;
+ final AttributeType<V> resultType;
/**
* The name of all feature properties that are known to be read by the
expression.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
index 9890426a4e..d56a79c3d1 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
@@ -22,6 +22,7 @@ import org.opengis.util.GenericName;
import org.opengis.util.FactoryException;
import org.opengis.util.InternationalString;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.util.Classes;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.collection.WeakHashSet;
import org.apache.sis.util.resources.Errors;
@@ -35,6 +36,7 @@ import org.opengis.feature.Feature;
import org.opengis.feature.Operation;
import org.opengis.feature.PropertyType;
import org.opengis.feature.AttributeType;
+import org.opengis.feature.IdentifiedType;
import org.opengis.feature.FeatureAssociationRole;
import org.opengis.filter.Expression;
@@ -104,7 +106,7 @@ import org.opengis.filter.Expression;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
* @since 0.7
*/
public final class FeatureOperations {
@@ -312,7 +314,7 @@ public final class FeatureOperations {
*
* @since 1.4
*/
- public static <V> Operation function(final Map<String,?> identification,
+ public static <V> Operation function(final Map<String, ?> identification,
final Function<? super Feature, ?
extends V> expression,
final AttributeType<? super V>
resultType)
{
@@ -341,13 +343,60 @@ public final class FeatureOperations {
*
* @since 1.4
*/
- public static <V> Operation expression(final Map<String,?> identification,
+ public static <V> Operation expression(final Map<String, ?> identification,
final Expression<? super Feature,
?> expression,
final AttributeType<V> resultType)
{
return function(identification,
expression.toValueType(resultType.getValueClass()), resultType);
}
+ /**
+ * Creates an operation with the same identification and result type than
the given operation,
+ * but evaluated using the given expression. For example, if the given
operation is the result
+ * of a previous call to {@link #expression expression(…)}, then invoking
this method is equivalent
+ * to invoking {@code expression(…)} again with the same arguments except
for {@code expression}.
+ *
+ * @param operation the operation to evaluate in a different way.
+ * @param expression the new expression to use for evaluating the
operation.
+ * @return the new operation. May be the given operation if the expression
is the same.
+ * @throws IllegalArgumentException if the {@linkplain
Operation#getResult() result type}
+ * of the given operation is not an {@link AttributeType}.
+ *
+ * @since 1.6
+ */
+ public static Operation replace(final PropertyType property, final
Expression<? super Feature, ?> expression) {
+ final AttributeType<?> resultType;
+ if (property instanceof ExpressionOperation<?>) {
+ var operation = (ExpressionOperation) property;
+ if (operation.expression.equals(expression)) {
+ return operation;
+ }
+ resultType = operation.resultType;
+ } else if (property instanceof AttributeType<?>) {
+ resultType = (AttributeType<?>) property;
+ } else if (property instanceof Operation) {
+ final IdentifiedType type = ((Operation) property).getResult();
+ if (type instanceof AttributeType<?>) {
+ resultType = (AttributeType<?>) type;
+ } else {
+ throw
illegalResultType(Errors.Keys.IllegalPropertyValueClass_3, property.getName(),
AttributeType.class, type);
+ }
+ } else {
+ throw illegalResultType(Errors.Keys.IllegalArgumentClass_2,
"property", property);
+ }
+ return expression(Map.of(AbstractIdentifiedType.INHERIT_FROM_KEY,
property), expression, resultType);
+ }
+
+ /**
+ * Returns the exception to throw for an illegal result type.
+ * The last argument will be replaced by the class or interface of that
argument.
+ */
+ private static IllegalArgumentException illegalResultType(final short key,
final Object... arguments) {
+ final int last = arguments.length - 1;
+ arguments[last] =
Classes.getStandardType(Classes.getClass(arguments[last]));
+ return new IllegalArgumentException(Errors.format(key, arguments));
+ }
+
/**
* Returns an expression for fetching the values of properties identified
by the given type.
* The returned expression will be the first of the following choices
which is applicable:
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
index d87f934aba..dc27495cde 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
@@ -29,6 +29,7 @@ import org.opengis.metadata.quality.Result;
import org.apache.sis.util.iso.Names;
import org.apache.sis.util.iso.DefaultNameFactory;
import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.pending.jdk.JDK16;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.metadata.maintenance.ScopeCode;
@@ -49,7 +50,7 @@ import org.opengis.feature.PropertyType;
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.5
+ * @version 1.6
* @since 0.5
*/
public final class Features {
@@ -212,6 +213,18 @@ public final class Features {
return (types != null) ? CommonParentFinder.select(types) : null;
}
+ /**
+ * Returns the name of all properties (including inherited properties) of
the given feature type.
+ *
+ * @param feature the feature type from which to get the name of all
properties.
+ * @return name of all properties of the specified feature type.
+ *
+ * @since 1.6
+ */
+ public static List<String> getPropertyNames(final FeatureType feature) {
+ return JDK16.toList(feature.getProperties(true).stream().map((p) ->
p.getName().toString()));
+ }
+
/**
* Returns the type of values provided by the given property. For
{@linkplain AttributeType attributes}
* (which is the most common case), the value type is given by {@link
AttributeType#getValueClass()}.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
index bfd7549cc7..7c997f3072 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
@@ -609,11 +609,11 @@ public class FeatureTypeBuilder extends TypeBuilder {
/**
* Returns a view of all attributes and associations added to the {@code
FeatureType} to build.
- * This list contains only properties declared explicitly to this builder;
- * it does not include properties inherited from {@linkplain
#getSuperTypes() super-types}.
+ * This list contains only properties declared explicitly to this builder.
+ * It does not include properties inherited from {@linkplain
#getSuperTypes() super-types}.
* The returned list is <em>live</em>: changes in this builder are
reflected in that list and conversely.
- * However, the returned list allows only {@linkplain List#remove(Object)
remove} operations;
- * new attributes or associations can be added only by calls to one of the
{@code addAttribute(…)}
+ * However, the returned list allows only {@linkplain List#remove(Object)
remove} operations.
+ * New attributes or associations can be added only by calls to one of the
{@code addAttribute(…)}
* or {@code addAssociation(…)} methods. Removal operations never affect
the super-types.
*
* @return a live list over the properties declared to this builder.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
index deac884387..3d6dbc3552 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
@@ -19,6 +19,7 @@ package org.apache.sis.feature.internal.shared;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
+import java.util.HashSet;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Objects;
@@ -28,14 +29,24 @@ import java.util.function.UnaryOperator;
import org.opengis.util.GenericName;
import org.apache.sis.util.Debug;
import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.CorruptedObjectException;
import org.apache.sis.util.internal.shared.UnmodifiableArrayList;
+import org.apache.sis.pending.jdk.Record;
+import org.apache.sis.pending.jdk.JDK19;
+import org.apache.sis.feature.Features;
+import org.apache.sis.feature.FeatureOperations;
+import org.apache.sis.feature.AbstractOperation;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.feature.builder.PropertyTypeBuilder;
import org.apache.sis.filter.visitor.ListingPropertyVisitor;
import org.apache.sis.io.TableAppender;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
import org.opengis.feature.Operation;
import org.opengis.filter.Expression;
import org.opengis.filter.Literal;
@@ -50,7 +61,7 @@ import org.opengis.filter.ValueReference;
* @author Guilhem Legal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
*/
-public final class FeatureProjection implements UnaryOperator<Feature> {
+public final class FeatureProjection extends Record implements
UnaryOperator<Feature> {
/**
* The type of features with the properties explicitly requested by the
user.
* The property names may differ from the properties of the {@link
FeatureProjectionBuilder#source() source}
@@ -72,7 +83,12 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
* The property names are the same as {@link #typeRequested} (i.e., may be
aliases).
* However, some operations may be wrapped in {@link OperationView}.
*
- * @see #dependencies()
+ * <p>This is <em>not</em> a container listing the properties of the
source feature that are required.
+ * This type still assume that the {@link #apply(Feature)} method will
receive complete source features.
+ * This type may differ from {@link #typeRequested} only when the latter
contains operation that have not
+ * been converted to stored attributes.</p>
+ *
+ * @see #requiredSourceProperties()
*/
public final FeatureType typeWithDependencies;
@@ -97,6 +113,28 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
*/
private final boolean createInstance;
+ /**
+ * Creates a new projection with the given properties.
+ *
+ * @param typeRequested type of features with the properties
explicitly requested by the user.
+ * @param typeWithDependencies requested type augmented with dependencies
required for the execution of operations.
+ * @param propertiesToCopy names of the properties to be stored in
the feature instances created by this object.
+ * @param expressions expressions to apply on the source feature
for fetching the property values.
+ * @param createInstance whether the {@link #apply(Feature)} method
shall create the feature instances.
+ */
+ private FeatureProjection(final FeatureType typeRequested,
+ final FeatureType typeWithDependencies,
+ final String[] propertiesToCopy,
+ final Expression<? super Feature, ?>[]
expressions,
+ final boolean createInstance)
+ {
+ this.typeRequested = typeRequested;
+ this.typeWithDependencies = typeWithDependencies;
+ this.propertiesToCopy = propertiesToCopy;
+ this.expressions = expressions;
+ this.createInstance = createInstance;
+ }
+
/**
* Creates a new projection with the given properties specified by a
builder.
* The {@link #apply(Feature)} method will copy the properties of the given
@@ -132,40 +170,17 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
this.expressions = ArraysExt.resize(expressions, storedCount);
}
- /**
- * Creates a new projection with a subset of the properties of another
projection.
- * This constructor is invoked when the caller handles itself some of the
properties.
- *
- * <h4>Behavioral change</h4>
- * Projections created by this constructor assumes that the feature
instances given to the
- * {@link #apply(Feature)} method are already instances of {@link
#typeWithDependencies}
- * and can be modified (if needed) in place. This constructor is designed
for cases where
- * the caller does itself a part of the {@code FeatureProjection} work.
- *
- * @param parent the projection from which to inherit the types and
expressions.
- * @param remaining index of the properties that still need to be copied
after the caller did its processing.
- *
- * @see #forPreexistingFeatureInstances(int[])
- */
- @SuppressWarnings({"rawtypes", "unchecked"})
- private FeatureProjection(final FeatureProjection parent, final int[]
remaining) {
- createInstance = false;
- typeRequested = parent.typeRequested;
- typeWithDependencies = parent.typeWithDependencies;
- expressions = new Expression[remaining.length];
- propertiesToCopy = new String[remaining.length];
- for (int i=0; i<remaining.length; i++) {
- final int index = remaining[i];
- propertiesToCopy[i] = parent.propertiesToCopy[index];
- expressions[i] = parent.expressions[index];
- }
- }
-
/**
* Returns a variant of this projection where the caller has created the
target feature instance itself.
* The callers may have set some property values itself, and the {@code
remaining} argument gives the
* indexes of the properties that still need to be copied after caller's
processing.
*
+ * <h4>Recommendation</h4>
+ * Caller should ensure that the {@code remaining} array does not contain
indexes <var>i</var>
+ * where {@code expressions[i]} is equivalent to {@code
FilterFactory.property(propertiesToCopy[i])}
+ * because it would be a useless operation. This method does not perform
that verification by itself
+ * on the assumption that it would duplicate work already done by the
caller.
+ *
* @param remaining index of the properties that still need to be copied
after the caller did its processing.
* @return a variant of this projection which only completes the
projection done by the caller,
* or {@code null} if there is nothing left to complete.
@@ -174,34 +189,133 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
if (remaining.length == 0 && typeRequested == typeWithDependencies) {
return null;
}
- return new FeatureProjection(this, remaining);
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ final Expression<? super Feature, ?>[] filteredExpressions = new
Expression[remaining.length];
+ final String[] filteredProperties = new String[remaining.length];
+ for (int i=0; i<remaining.length; i++) {
+ final int index = remaining[i];
+ filteredProperties [i] = propertiesToCopy[index];
+ filteredExpressions[i] = expressions[index];
+ }
+ return new FeatureProjection(typeRequested, typeWithDependencies,
filteredProperties, filteredExpressions, false);
}
/**
* Creates a new projection with the same properties as the source
projection, but modified expressions.
- * The modifications are specified by the given {@code mapper}. No
expression can be added or removed.
- * New expressions should return values of the same type as the previous
expressions.
- * The new expressions shall not introduce new dependencies.
+ * The modifications are specified by the given {@code mapper}, which
should return values of the same
+ * type as the previous expressions. The new expressions shall not
introduce new dependencies.
*
* <h4>Purpose</h4>
- * This constructor is used when the caller can replace some expressions
by <abbr>SQL</abbr> statements.
+ * This method is used when the caller can replace some expressions by
<abbr>SQL</abbr> statements.
*
- * @param source the projection to copy.
* @param mapper a function receiving in arguments the property name and
the expression fetching the property value,
* and returning the expression to use in replacement of
the function given in argument.
+ * @return the feature projection with modified expressions. May be {@code
this} if there is no change.
*/
- public FeatureProjection(final FeatureProjection source,
+ public FeatureProjection replaceExpressions(
final BiFunction<String, Expression<? super Feature, ?>,
Expression<? super Feature, ?>> mapper)
{
- typeRequested = source.typeRequested;
- createInstance = source.createInstance;
- propertiesToCopy = source.propertiesToCopy;
- expressions = source.expressions.clone();
+ final Map<String, Expression<? super Feature, ?>> filtered =
JDK19.newLinkedHashMap(expressions.length);
for (int i = 0; i < expressions.length; i++) {
- expressions[i] = mapper.apply(propertiesToCopy[i], expressions[i]);
+ final String property = propertiesToCopy[i];
+ if (filtered.put(property, mapper.apply(property, expressions[i]))
!= null) {
+ throw new
CorruptedObjectException(Errors.format(Errors.Keys.DuplicatedElement_1,
property));
+ }
+ }
+ /*
+ * The above loop replaced the expressions used for fetching values
from the source feature instances
+ * and storing them as attributes. But expressions may also be used in
an indirect way, as operations.
+ * Note that operations have no corresponding entries in the
`propertiesToCopy` array.
+ */
+ final var builder = new FeatureTypeBuilder(typeWithDependencies);
+ for (final PropertyTypeBuilder property :
builder.properties().toArray(PropertyTypeBuilder[]::new)) {
+ filtered.computeIfAbsent(property.getName().toString(), (name) -> {
+ final PropertyType type = property.build();
+ Expression<? super Feature, ?> expression =
FeatureOperations.expressionOf(type);
+ if (!expression.equals(expression = mapper.apply(name,
expression))) {
+
property.replaceBy(builder.addProperty(FeatureOperations.replace(type,
expression)));
+ return expression;
+ }
+ return null; // No change, keep the current operation.
+ });
+ }
+ /*
+ * Some expressions may become unnecessary if the new expression only
fetches a property value, then stores
+ * that value unchanged in the same feature instance. Note that we
will need to remove only the expression,
+ * not the property in the `FeatureType`.
+ */
+ if (!createInstance) {
+ for (final var it = filtered.entrySet().iterator(); it.hasNext();)
{
+ final var entry = it.next();
+ final var expression = entry.getValue();
+ if (expression instanceof ValueReference<?,?>) {
+ final String name = ((ValueReference<?,?>)
expression).getXPath();
+ if (name.equals(entry.getKey())) {
+ // The expression reads and stores a property of the
same name in the same feature instance.
+ final PropertyTypeBuilder property =
builder.getProperty(name);
+ if (property != null) { // A null value would
probably be a bug, but check anyway.
+ property.remove();
+ it.remove();
+ }
+ }
+ }
+ }
+ }
+ /*
+ * Maybe the new expressions have less dependencies than the old
expressions. It happens if `mapper` replaced
+ * a pure Java filter by a SQL expression such as `SQRT(x*x + y*y)`,
in which case the `x` and `y` properties
+ * are used by the database and do not need anymore to be forwarded to
the Java code.
+ */
+ if (typeWithDependencies != typeRequested) {
+ final var unnecessary = new
HashSet<>(Features.getPropertyNames(typeWithDependencies));
+ unnecessary.removeAll(Features.getPropertyNames(typeRequested));
+ for (var property : builder.properties()) {
+ final PropertyType type = property.build();
+ if (type instanceof AbstractOperation) {
+ unnecessary.removeAll(((AbstractOperation)
type).getDependencies());
+ }
+ }
+ for (final String name : unnecessary) {
+ final PropertyTypeBuilder property = builder.getProperty(name);
+ // A `false` value below would be a bug, but check anyway.
+ if (property != null && filtered.remove(name) != null) {
+ property.remove();
+ }
+ }
+ }
+ /*
+ * Remove any remaining operations. The remaining entries shall be
only expressions
+ * for fetching the values to store in attributes, not values computed
on-the-fly.
+ */
+ filtered.keySet().removeIf((name) -> {
+ final PropertyTypeBuilder property = builder.getProperty(name);
+ return (property != null) && (property.build() instanceof
Operation);
+ });
+ /*
+ * Create the new feature types with the modified expressions.
+ * Check if we can reuse the existing feature types.
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ final Expression<? super Feature, ?>[] filteredExpressions =
filtered.values().toArray(Expression[]::new);
+ String[] filteredProperties = filtered.keySet().toArray(String[]::new);
+ if (Arrays.equals(filteredProperties, propertiesToCopy)) {
+ filteredProperties = propertiesToCopy; // Share existing
instances (common case).
+ }
+ FeatureType withDeps = builder.build();
+ boolean modified =
builder.setName(typeRequested.getName()).properties().removeIf(
+ (property) ->
!typeRequested.hasProperty(property.getName().toString()));
+ FeatureType filteredType = builder.build();
+ if (filteredType.equals(typeRequested)) {
+ filteredType = typeRequested;
+ }
+ if (!modified) {
+ withDeps = filteredType;
+ } else if (withDeps.equals(typeWithDependencies)) {
+ withDeps = typeWithDependencies;
}
- // TODO: check if we can remove some dependencies.
- typeWithDependencies = source.typeWithDependencies;
+ var p = new FeatureProjection(filteredType, withDeps,
filteredProperties, filteredExpressions, createInstance);
+ if (equals(p)) p = this;
+ return p;
}
/**
@@ -266,9 +380,9 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
* dependencies are references to {@link #typeWithDependencies} properties
instead of properties of the
* source features. The property names may differ.</p>
*
- * @return all dependencies (including transitive dependencies) as XPaths.
+ * @return all dependencies (including transitive dependencies) from
source features, as XPaths.
*/
- public Set<String> dependencies() {
+ public Set<String> requiredSourceProperties() {
Set<String> references = null;
for (var expression : expressions) {
references = ListingPropertyVisitor.xpaths(expression, references);
diff --git
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
index 282f42986c..d2d84de843 100644
---
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
+++
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.feature.internal.shared;
+import java.util.Arrays;
import java.util.Set;
import java.util.HashSet;
import org.apache.sis.util.iso.Names;
@@ -128,7 +129,7 @@ public final class FeatureProjectionTest extends TestCase {
final FeatureProjection projection = builder.project().orElseThrow();
assertSame(projection.typeRequested, projection.typeWithDependencies);
assertPropertyNamesEqual(projection.typeRequested, "country",
"population");
- assertSetEquals(Set.of("name", "population"),
projection.dependencies());
+ assertSetEquals(Arrays.asList("name", "population"),
projection.requiredSourceProperties());
assertValueClassEquals(String.class, projection, "country");
return projection;
}
@@ -205,6 +206,9 @@ public final class FeatureProjectionTest extends TestCase {
/**
* Tests the creation of a feature with an additional property computed
early.
* The operation is computed immediately from the source feature type.
+ *
+ * Note that since the results are stored (i.e. the "density" operation
become an attribute in the
+ * final feature instance), there is no need for intermediate feature type
with extra dependencies.
*/
@Test
public void testSubsetWithStoredOperation() {
@@ -214,12 +218,14 @@ public final class FeatureProjectionTest extends TestCase
{
addPopulationDensity(builder, true);
builder.setName("Population density");
final FeatureProjection projection = builder.project().orElseThrow();
- assertSame(projection.typeRequested, projection.typeWithDependencies);
- assertSetEquals(Set.of("name", "population", "area"),
projection.dependencies());
assertPropertyNamesEqual(projection.typeRequested, "country",
"population", "density");
+ assertSetEquals(Arrays.asList("name", "population", "area"),
projection.requiredSourceProperties());
+
+ // `typeWithDependencies` does not have the "area" extra dependency
because values are stored.
+ assertSame(projection.typeRequested, projection.typeWithDependencies);
// Property is an attribute because we requested the "stored" mode.
- assertInstanceOf(AttributeType.class,
projection.typeWithDependencies.getProperty("density"));
+ assertInstanceOf(AttributeType.class,
projection.typeRequested.getProperty("density"));
assertValueClassEquals(Number.class, projection, "density");
verifyDensityOnFeatureInstance(projection);
}
@@ -236,10 +242,10 @@ public final class FeatureProjectionTest extends TestCase
{
addPopulationDensity(builder, false);
builder.setName("Population density");
final FeatureProjection projection = builder.project().orElseThrow();
- assertNotEquals(projection.typeRequested,
projection.typeWithDependencies);
- assertSetEquals(Set.of("name", "population", "area"),
projection.dependencies());
+ assertSetEquals(Arrays.asList("name", "population", "area"),
projection.requiredSourceProperties());
assertPropertyNamesEqual(projection.typeRequested, "country",
"population", "density");
assertPropertyNamesEqual(projection.typeWithDependencies, "country",
"population", "density", "area");
+ // `typeWithDependencies` needs the extra "area" dependency because
values are computed.
// Property is an operation because we requested the "deferred" mode.
assertInstanceOf(Operation.class,
projection.typeWithDependencies.getProperty("density"));
@@ -251,31 +257,74 @@ public final class FeatureProjectionTest extends TestCase
{
/**
* Tests the creation of a feature where an operation has been replaced by
a simpler one.
- * This case happen when a pure-Java operation has been replaced by a
<abbr>SQL</abbr> expression,
+ * This case happens when a pure-Java operation has been replaced by a
<abbr>SQL</abbr> expression.
+ * The <abbr>SQL</abbr> expression is simulated by a literal, which
removes the dependency to the
+ * "area" property when an instance of {@link
FeatureProjection#typeRequested} is created from a
+ * source feature instance.
+ */
+ @Test
+ public void testSubsetWithReplacedOperation() {
+ final FeatureProjection projection =
testSubsetWithReplacedOperation(true,
+ new HashSet<>(Arrays.asList("country", "population",
"density")));
+
+ // Property is an operation because we requested the "stored" mode.
+ assertInstanceOf(AttributeType.class,
projection.typeWithDependencies.getProperty("density"));
+
+ // Compared to `testSubsetWithStoredOperation`, the "area" property
disaspear.
+ assertSetEquals(Arrays.asList("name", "population"),
projection.requiredSourceProperties());
+
+ // No extra dependency.
+ assertSame(projection.typeRequested, projection.typeWithDependencies);
+ }
+
+
+ /**
+ * Tests the creation of a feature where an operation has been replaced by
a simpler one.
+ * This case happens when a pure-Java operation has been replaced by a
<abbr>SQL</abbr> expression,
* in which case the expression is simpler from <abbr>SIS</abbr>
perspective. A consequence of this
* simplification is that it may remove the need for some dependencies.
*/
@Test
- public void testSubsetWithReplacedOperation() {
+ public void testSubsetWithReplacedOperationAndLessDependencies() {
+ final FeatureProjection projection =
testSubsetWithReplacedOperation(false,
+ new HashSet<>(Arrays.asList("country", "population",
"density", "area")));
+
+ // Property is an operation because we requested the "deferred" mode.
+ assertInstanceOf(Operation.class,
projection.typeWithDependencies.getProperty("density"));
+
+ // Compared to `testSubsetWithDeferredOperation`, the "area" property
disaspear.
+ assertSetEquals(Arrays.asList("name", "population"),
projection.requiredSourceProperties());
+
+ // The extra "area" dependency should have been removed, resulting in
the same type.
+ assertSame(projection.typeRequested, projection.typeWithDependencies);
+ }
+
+ /**
+ * Implementation of the {@code testSubsetWithReplacedOperation()} test
cases.
+ * The "density" operation will be resolved as an attribute if {@code
stored} is {@code true}.
+ *
+ * @param stored whether the "density" operation should be converted
to an attribute.
+ * @param attributes names of the attributes that are expected to found
in the feature.
+ * @return the projection with modified expressions.
+ */
+ private FeatureProjection testSubsetWithReplacedOperation(final boolean
stored, final Set<String> attributes) {
final var builder = new FeatureProjectionBuilder(source, null);
addCountry(builder);
addPopulation(builder, false);
- addPopulationDensity(builder, true);
+ addPopulationDensity(builder, stored);
builder.setName("Population density");
FeatureProjection projection = builder.project().orElseThrow();
- final Set<String> expected = new HashSet<>(Set.of("country",
"population", "density"));
- projection = new FeatureProjection(projection, (name, expression) -> {
- assertTrue(expected.remove(name), name);
+ projection = projection.replaceExpressions((name, expression) -> {
+ assertTrue(attributes.remove(name), name);
if (name.equals("density")) {
return ff.literal(4.08);
}
return expression;
});
- assertTrue(expected.isEmpty(), expected.toString());
- assertSame(projection.typeRequested, projection.typeWithDependencies);
// Because no more extra dependency.
- assertSetEquals(Set.of("name", "population"),
projection.dependencies());
assertPropertyNamesEqual(projection.typeRequested, "country",
"population", "density");
+ assertTrue(attributes.isEmpty(), attributes.toString());
verifyDensityOnFeatureInstance(projection);
+ return projection;
}
/**
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
index fefc2b0473..b475083191 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
@@ -61,7 +61,7 @@ public class Column implements Cloneable {
public final String name;
/**
- * Name of the column as declared in with a {@code AS} clause in the
<abbr>SQL</abbr> statement.
+ * Name of the column as declared with a {@code AS} clause in the
<abbr>SQL</abbr> statement.
* This is never null but may be identical to {@link #name} if no {@code
AS} clause was specified.
*
* @see ResultSetMetaData#getColumnLabel(int)
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
index e2c49b3962..bdc5cb16a6 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
@@ -461,7 +461,7 @@ final class FeatureStream extends DeferredStream<Feature> {
final var columnSQL = new SelectionClause(projected);
@SuppressWarnings("LocalVariableHidesMemberVariable")
final SelectionClauseWriter filterToSQL = getFilterToSQL();
- queriedProjection = new FeatureProjection(queriedProjection,
(name, expression) -> {
+ queriedProjection =
queriedProjection.replaceExpressions((name, expression) -> {
final JDBCType type = filterToSQL.writeFunction(columnSQL,
expression);
if (type != null) try {
columnSQL.append(" AS ").appendIdentifier(name);
@@ -473,9 +473,6 @@ final class FeatureStream extends DeferredStream<Feature> {
columnSQL.clear();
return expression;
});
- if (queriedProjection.equals(projection)) {
- queriedProjection = projection; // No change, keep the
original one.
- }
}
/*
* Build a pseudo-table (a view) with the subset of columns
specified by the projection.
@@ -484,7 +481,7 @@ final class FeatureStream extends DeferredStream<Feature> {
final var reusedNames = new HashSet<String>();
projected = new Table(projected, queriedProjection, reusedNames,
unhandled);
completion =
queriedProjection.forPreexistingFeatureInstances(unhandled.stream().toArray());
- if (completion != null &&
!reusedNames.containsAll(completion.dependencies())) {
+ if (completion != null &&
!reusedNames.containsAll(completion.requiredSourceProperties())) {
/*
* Cannot use `projected` because some expressions need
properties available only
* in the source features. Request full feature instances from
the original table.