This is an automated email from the ASF dual-hosted git repository. amanin pushed a commit to branch fix/fast-envelope in repository https://gitbox.apache.org/repos/asf/sis.git
commit 09430ce1a0ee2289911a626474d0957c090bd81f Author: Alexis Manin <[email protected]> AuthorDate: Mon Jan 20 18:18:46 2020 +0100 fix(SQLStore): Add a feature to ask database for an estimate of a dataset envelope (table, simple query) --- .../main/java/org/apache/sis/feature/Features.java | 191 +++++++++++++++++++-- .../sis/internal/sql/feature/ColumnAdapter.java | 14 ++ .../sis/internal/sql/feature/FeatureAdapter.java | 32 +++- .../sis/internal/sql/feature/PostGISMapping.java | 97 ++++++++++- .../org/apache/sis/internal/sql/feature/Table.java | 26 ++- 5 files changed, 336 insertions(+), 24 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java b/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java index 16f1b7d..1b27109 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java @@ -16,32 +16,42 @@ */ package org.apache.sis.feature; +import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.IdentityHashMap; -import org.opengis.util.GenericName; -import org.opengis.util.NameFactory; -import org.opengis.util.InternationalString; -import org.opengis.metadata.maintenance.ScopeCode; -import org.opengis.metadata.quality.ConformanceResult; -import org.opengis.metadata.quality.DataQuality; -import org.opengis.metadata.quality.Element; -import org.opengis.metadata.quality.Result; -import org.apache.sis.util.Static; -import org.apache.sis.util.iso.DefaultNameFactory; -import org.apache.sis.internal.system.DefaultFactories; -import org.apache.sis.internal.feature.Resources; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.stream.Collectors; -// Branch-dependent imports import org.opengis.feature.Attribute; import org.opengis.feature.AttributeType; import org.opengis.feature.Feature; -import org.opengis.feature.FeatureType; import org.opengis.feature.FeatureAssociationRole; +import org.opengis.feature.FeatureType; import org.opengis.feature.IdentifiedType; import org.opengis.feature.InvalidPropertyValueException; import org.opengis.feature.Operation; +import org.opengis.feature.PropertyNotFoundException; import org.opengis.feature.PropertyType; +import org.opengis.metadata.maintenance.ScopeCode; +import org.opengis.metadata.quality.ConformanceResult; +import org.opengis.metadata.quality.DataQuality; +import org.opengis.metadata.quality.Element; +import org.opengis.metadata.quality.Result; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.util.GenericName; +import org.opengis.util.InternationalString; +import org.opengis.util.NameFactory; + +import org.apache.sis.internal.feature.AttributeConvention; +import org.apache.sis.internal.feature.Resources; +import org.apache.sis.internal.system.DefaultFactories; +import org.apache.sis.util.Static; +import org.apache.sis.util.iso.DefaultNameFactory; +import org.apache.sis.util.logging.Logging; + +// Branch-dependent imports /** @@ -55,6 +65,13 @@ import org.opengis.feature.PropertyType; * @module */ public final class Features extends Static { + + /** + * A test to know if a given property is an SIS convention or not. Return true if + * the property is NOT marked as an SIS convention, false otherwise. + */ + private static final Predicate<IdentifiedType> IS_NOT_CONVENTION = p -> !AttributeConvention.contains(p.getName()); + /** * Do not allow instantiation of this class. */ @@ -306,4 +323,148 @@ public final class Features extends Static { } } } + + + /** + * Search for the main geometric property in the given type. We'll search + * for an SIS convention first (see + * {@link AttributeConvention#GEOMETRY_PROPERTY}. If no convention is set on + * the input type, we'll check if it contains a single geometric property. + * If it's the case, we return it. Otherwise (no or multiple geometries), we + * throw an exception. + * + * @param type The data type to search into. + * @return The main geometric property we've found. It will never be null. If no geometric property is found, an + * exception is thrown. + * @throws PropertyNotFoundException If no geometric property is available + * in the given type. + * @throws IllegalStateException If no convention is set (see + * {@link AttributeConvention#GEOMETRY_PROPERTY}), and we've found more than + * one geometry. + */ + public static PropertyType getDefaultGeometry(final FeatureType type) throws PropertyNotFoundException, IllegalStateException { + PropertyType geometry; + try { + geometry = type.getProperty(AttributeConvention.GEOMETRY_PROPERTY.toString()); + } catch (PropertyNotFoundException e) { + try { + geometry = searchForGeometry(type); + } catch (RuntimeException e2) { + e2.addSuppressed(e); + throw e2; + } + } + + return geometry; + } + + /** + * Search for a geometric attribute outside SIS conventions. More accurately, + * we expect the given type to have a single geometry attribute. If many are + * found, an exception is thrown. + * + * @param type The data type to search into. + * @return The only geometric property we've found. + * @throws PropertyNotFoundException If no geometric property is available in + * the given type. + * @throws IllegalStateException If we've found more than one geometry. + */ + private static PropertyType searchForGeometry(final FeatureType type) throws PropertyNotFoundException, IllegalStateException { + final List<? extends PropertyType> geometries = type.getProperties(true).stream() + .filter(IS_NOT_CONVENTION) + .filter(AttributeConvention::isGeometryAttribute) + .collect(Collectors.toList()); + + if (geometries.size() < 1) { + throw new PropertyNotFoundException("No geometric property can be found outside of sis convention."); + } else if (geometries.size() > 1) { + throw new IllegalStateException("Multiple geometries found. We don't know which one to select."); + } else { + return geometries.get(0); + } + } + + /** + * Test if given property type is an attribute as defined by {@link AttributeType}, or if it produces one as an + * {@link Operation#getResult() operation result}. It it is, we return the found attribute. + * + * @param input the data type to unravel the attribute from. + * @return The found attribute or an empty shell if we cannot find any. + */ + public static Optional<AttributeType<?>> castOrUnwrap(IdentifiedType input) { + // In case an operation also implements attribute type, we check it first. + // TODO : cycle detection ? + while (!(input instanceof AttributeType) && input instanceof Operation) { + input = ((Operation) input).getResult(); + } + + if (input instanceof AttributeType) { + return Optional.of((AttributeType) input); + } + + return Optional.empty(); + } + + /** + * Search for a specific characteristic in given property. Note that if the property is not an attribute, we'll try + * to get one for the search (see {@link #castOrUnwrap(IdentifiedType)} for more details). + * + * @param type The property to search into. + * @param characteristicName The name of the searched characteristic. + * @return Found characteristic, or nothing if we cannot find any matching given name. + */ + public static Optional<AttributeType> getCharacteristic(PropertyType type, String characteristicName) { + return castOrUnwrap(type) + .map(attr -> attr.characteristics().get(characteristicName)); + } + + /** + * Search for a characteristic value in a given property. For more complete information, you can get the complete + * characteristic definition through {@link #getCharacteristic(PropertyType, String)}. + * + * @param type The property to search into + * @param characteristicName Name of the characteristic to get a value for. + * @param <T> Expected type for characteristics values. Be careful, if using a wrong type, an error could occur on + * execution. + * @return The default value of the characteristic if we've found it, or an empty shell if the characteristic does + * not exists or has no default value. + * @throws ClassCastException If a value is found, but does not match specified data type. + */ + public static <T> Optional<T> getCharacteristicValue(PropertyType type, String characteristicName) { + return getCharacteristic(type, characteristicName) + .map(characteristic -> (T) characteristic.getDefaultValue()); + } + + /** + * Extract the coordinate reference system associated to the primary geometry of input data type. + * + * @implNote + * Primary geometry is determined using {@link #getDefaultGeometry(org.opengis.feature.FeatureType) }. + * + * @param type The data type to extract reference system from. + * @return The CRS associated to the default geometry of this data type, or + * an empty value if we cannot determine what is the primary geometry of the + * data type. Note that an empty value is also returned if a geometry property + * is found, but no CRS characteristics is associated with it. + */ + public static Optional<CoordinateReferenceSystem> getDefaultCrs(FeatureType type) { + try { + return getDefaultCrs(getDefaultGeometry(type)); + } catch (IllegalArgumentException | IllegalStateException ex) { + Logging.getLogger("org.apache.sis.feature").log(Level.FINE, "Cannot extract CRS from type, cause no default geometry is available", ex); + //no default geometry property + return Optional.empty(); + } + } + + /** + * Extract CRS characteristic if it exists. + * + * @param type The property that we want information for. + * @return If any Coordinate reference system characteristic (as defined by {@link AttributeConvention#CRS_CHARACTERISTIC SIS convention}) + * is available, its default value is returned. Otherwise, nothing. + */ + public static Optional<CoordinateReferenceSystem> getDefaultCrs(PropertyType type) { + return getCharacteristicValue(type, AttributeConvention.CRS_CHARACTERISTIC.toString()); + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java index 8f2387d..e646298 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ColumnAdapter.java @@ -18,8 +18,10 @@ package org.apache.sis.internal.sql.feature; import java.sql.Connection; import java.sql.ResultSet; +import java.sql.SQLException; import java.util.Optional; +import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; @@ -55,6 +57,18 @@ public interface ColumnAdapter<T> { } /** + * If possible, return a fast estimation of the dataset envelope based on this column geometry. + * + * @implNote Note that no cache system should be implemented here, as we query a valid connection for direct + * computation. However, containers are free to cache returned information at their convenience. + * + * @param target The database connection to use to compute the envelope. + * @return If available, the envelope of the current column. Otherwise, an empty shell. + * @throws SQLException If we fail fetching information from database. + */ + default Optional<Envelope> getEnvelope(final Connection target) throws SQLException { return Optional.empty(); } + + /** * * @return The (possibly parent) type of objects read by this mapper. Note that it MUST NOT return null values. */ diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java index 061e9d2..9668b55 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAdapter.java @@ -22,10 +22,16 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; +import org.opengis.geometry.Envelope; + +import org.apache.sis.internal.feature.Geometries; +import org.apache.sis.util.collection.BackingStoreException; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; @@ -75,6 +81,30 @@ class FeatureAdapter { return new ResultSetAdapter(rtu); } + + public Optional<Envelope> getEnvelope(final Connection target) throws SQLException { + // TODO: Allow user to specify a specific column + try { + return attributeMappers.stream() + .map(mapper -> mapper.fetchValue) + .filter(column -> Geometries.isKnownType(column.getJavaType())) + .map(column -> getEnvelope(target, column)) + .filter(Objects::nonNull) + // TODO: that's not accurate, but has the benefit to avoid heavy-weight trasforms in a common-space. + .findAny(); + } catch (BackingStoreException e) { + throw e.unwrapOrRethrow(SQLException.class); + } + } + + private Envelope getEnvelope(Connection target, ColumnAdapter<?> column) { + try { + return column.getEnvelope(target).orElse(null); + } catch (SQLException e) { + throw new BackingStoreException(e); + } + } + /** * Specialization of {@link FeatureAdapter} as a short-live object, able to use a database connection to load third- * party data. @@ -146,7 +176,7 @@ class FeatureAdapter { // nature, and an indexed implementation could (to verify, still) be bad on memory footprint. final String propertyName; final int columnIndex; - final ColumnAdapter fetchValue; + final ColumnAdapter<?> fetchValue; PropertyMapper(String propertyName, int columnIndex, ColumnAdapter fetchValue) { this.propertyName = propertyName; diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java index a2dffbf..4445234 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/PostGISMapping.java @@ -17,6 +17,7 @@ package org.apache.sis.internal.sql.feature; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -25,10 +26,15 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.geometry.DirectPosition2D; +import org.apache.sis.geometry.Envelope2D; import org.apache.sis.internal.feature.Geometries; import org.apache.sis.internal.metadata.sql.Dialect; +import org.apache.sis.internal.metadata.sql.SQLBuilder; +import org.apache.sis.referencing.CRS; import org.apache.sis.setup.GeometryLibrary; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.collection.Cache; @@ -109,11 +115,11 @@ public final class PostGISMapping implements DialectMapping { final Class geomClass = getGeometricClass(geometryType, library); if (geomDef == null || geomDef.crs == null) { - return new HexEWKBDynamicCrs(geomClass); + return new HexEWKBDynamicCrs(geomClass, definition); } else { // TODO: activate optimisation : WKB is lighter, but we need to modify user query, and to know CRS in advance. //geometryDecoder = new WKBReader(geomDef.crs); - return new HexEWKBFixedCrs(geomClass, geomDef.crs); + return new HexEWKBFixedCrs(geomClass, definition, geomDef.crs); } } @@ -154,14 +160,43 @@ public final class PostGISMapping implements DialectMapping { } } - private final class HexEWKBFixedCrs extends Reader { + private abstract class PostGisReader extends Reader { + + final SQLColumn columnDefinition; final CoordinateReferenceSystem crsToApply; - public HexEWKBFixedCrs(Class geomClass, CoordinateReferenceSystem crsToApply) { + public PostGisReader(Class geomClass, final SQLColumn colDef, CoordinateReferenceSystem crsToApply) { super(geomClass); + this.columnDefinition = colDef; this.crsToApply = crsToApply; } + /** + * @implNote Use PostGIS <a href="https://postgis.net/docs/ST_EstimatedExtent.html">ST_EstimatedExtent</a> + * function to get a rough estimation of column extent. If it returns no result, we fallback on + * <a href="https://postgis.net/docs/ST_Extent.html">ST_Extent</a> to get information. + * @return Envelope of the column if available, else nothing. + */ + @Override + public Optional<Envelope> getEnvelope(final Connection target) throws SQLException { + if (columnDefinition.origin != null) { + EnvelopeEstimator estimator = new EnvelopeEstimator(target, columnDefinition.naming, columnDefinition.origin, crsToApply); + + Optional<Envelope> env = estimator.execute(true); + if (!env.isPresent()) env = estimator.execute(false); + + return env; + } + return Optional.empty(); + } + } + + private final class HexEWKBFixedCrs extends PostGisReader { + + public HexEWKBFixedCrs(Class geomClass, final SQLColumn colDef, CoordinateReferenceSystem crsToApply) { + super(geomClass, colDef, crsToApply); + } + @Override public SQLBiFunction prepare(Connection target) { return new HexEWKBReader(new EWKBReader(library).forCrs(crsToApply)); @@ -173,10 +208,10 @@ public final class PostGISMapping implements DialectMapping { } } - private final class HexEWKBDynamicCrs extends Reader { + private final class HexEWKBDynamicCrs extends PostGisReader { - public HexEWKBDynamicCrs(Class geomClass) { - super(geomClass); + public HexEWKBDynamicCrs(Class geomClass, final SQLColumn colDef) { + super(geomClass, colDef, null); } @Override @@ -210,4 +245,52 @@ public final class PostGISMapping implements DialectMapping { return hexa == null ? null : reader.readHexa(hexa); } } + + private static final class EnvelopeEstimator { + final SQLBuilder builder; + final Connection target; + private final ColumnRef colRef; + private final TableReference table; + final CoordinateReferenceSystem crs; + + private EnvelopeEstimator(final Connection target, final ColumnRef colRef, final TableReference table, final CoordinateReferenceSystem optCrs) throws SQLException { + builder = new SQLBuilder(target.getMetaData(), false); + this.target = target; + this.colRef = colRef; + this.table = table; + + if (optCrs != null) { + crs = CRS.getHorizontalComponent(optCrs); + } else crs = null; + } + + private Optional<Envelope> execute(boolean fast) throws SQLException { + builder.clear(); + builder + .append("SELECT st_xmin(box) as minx, st_ymin(box) as miny, st_xmax(box) as maxx, st_ymax(box) as maxy") + .append(" FROM (") + .append("SELECT "+(fast ? "ST_EstimatedExtent" : "ST_Extent")+"(") + .appendIdentifier(colRef.getColumnName()) + .append(") as box FROM ") + .appendIdentifier(table.schema, table.table) + .append(") as sub") + .toString(); + + try ( + PreparedStatement query = target.prepareStatement("TODO"); + ResultSet dbResult = query.executeQuery() + ) { + if (dbResult.next()) { + final Envelope2D env = new Envelope2D( + new DirectPosition2D(dbResult.getDouble(1), dbResult.getDouble(2)), + new DirectPosition2D(dbResult.getDouble(3), dbResult.getDouble(4)) + ); + if (crs != null) env.setCoordinateReferenceSystem(crs); + return Optional.of(env); + } + } + + return Optional.empty(); + } + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java index 9669c29..606d858 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java @@ -34,7 +34,9 @@ import org.opengis.feature.AttributeType; import org.opengis.feature.Feature; import org.opengis.feature.FeatureAssociationRole; import org.opengis.feature.FeatureType; +import org.opengis.feature.PropertyNotFoundException; import org.opengis.feature.PropertyType; +import org.opengis.geometry.Envelope; import org.opengis.util.GenericName; import org.apache.sis.internal.metadata.sql.Reflection; @@ -196,7 +198,16 @@ final class Table extends AbstractFeatureSet { this.importedKeys = toArray(specification.getImports()); this.exportedKeys = toArray(specification.getExports()); this.primaryKeyClass = primaryKeys.length < 2 ? Object.class : Object[].class; - this.hasGeometry = specification.getPrimaryGeometryColumn().isPresent(); + boolean geometryFound; + try { + org.apache.sis.feature.Features.getDefaultGeometry(featureType); + geometryFound = true; + } catch (PropertyNotFoundException e) { + geometryFound= false; + } catch (IllegalStateException e) { + geometryFound = true; + } + this.hasGeometry = geometryFound; this.attributes = Collections.unmodifiableList( specification.getColumns().stream() .map(column -> column.naming) @@ -204,6 +215,19 @@ final class Table extends AbstractFeatureSet { ); } + + @Override + public Optional<Envelope> getEnvelope() throws DataStoreException { + if (hasGeometry) { + try (Connection target = Database.connectReadOnly(source)) { + return adapter.getEnvelope(target); + } catch (SQLException e) { + throw new DataStoreException("Database interrogation for an envelope failed", e); + } + } + return Optional.empty(); + } + @Override public FeatureSet subset(Query query) throws DataStoreException { if (query instanceof SimpleQuery) {
