This is an automated email from the ASF dual-hosted git repository. amanin pushed a commit to branch refactor/sql-store in repository https://gitbox.apache.org/repos/asf/sis.git
commit 237a71aba54f100c60bd4f2243f4967023b0bf09 Author: Alexis Manin <[email protected]> AuthorDate: Wed Nov 6 16:45:44 2019 +0100 feat(SQLStore): Add partial PostGIS support : geometries and geographies. --- .../java/org/apache/sis/internal/feature/ESRI.java | 23 +- .../apache/sis/internal/feature/Geometries.java | 22 +- .../java/org/apache/sis/internal/feature/JTS.java | 31 ++- .../org/apache/sis/internal/feature/Java2D.java | 14 +- .../org/apache/sis/internal/feature/jts/JTS.java | 2 +- .../sis/internal/sql/feature/ANSIInterpreter.java | 2 +- .../sis/internal/sql/feature/ANSIMapping.java | 35 +-- .../apache/sis/internal/sql/feature/Analyzer.java | 67 +++--- .../internal/sql/feature/CRSIdentification.java | 91 +++++++ .../sis/internal/sql/feature/ColumnAdapter.java | 65 +++-- .../apache/sis/internal/sql/feature/Database.java | 30 ++- .../sis/internal/sql/feature/DialectMapping.java | 21 +- .../sis/internal/sql/feature/EWKBReader.java | 261 +++++++++++++++++++++ .../sis/internal/sql/feature/FeatureAdapter.java | 82 +++++-- .../apache/sis/internal/sql/feature/Features.java | 6 +- .../sql/feature/GeometryIdentification.java | 159 +++++++++++++ .../sis/internal/sql/feature/PostGISMapping.java | 242 ++++++++++++++++++- .../sis/internal/sql/feature/QueryFeatureSet.java | 40 +--- .../sis/internal/sql/feature/SQLCloseable.java | 8 + .../apache/sis/internal/sql/feature/SQLColumn.java | 82 +++---- .../sis/internal/sql/feature/SpatialFunctions.java | 18 +- .../apache/sis/internal/sql/feature/StreamSQL.java | 5 +- .../org/apache/sis/internal/sql/feature/Table.java | 2 +- 23 files changed, 1078 insertions(+), 230 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java index c01dd00..cca1b26 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java @@ -18,6 +18,7 @@ package org.apache.sis.internal.feature; import java.nio.ByteBuffer; import java.util.Iterator; +import java.util.stream.Stream; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.math.Vector; @@ -150,7 +151,7 @@ final class ESRI extends Geometries<Geometry> { @Override public Geometry toPolygon(Geometry polyline) throws IllegalArgumentException { if (polyline instanceof Polygon) return polyline; - return createMultiPolygonImpl(polyline); + return createMultiPolygon(Stream.of(polyline)); } /** @@ -159,7 +160,7 @@ final class ESRI extends Geometries<Geometry> { * @throws ClassCastException if an element in the iterator is not an ESRI geometry. */ @Override - final Geometry tryMergePolylines(Object next, final Iterator<?> polylines) { + public final Geometry tryMergePolylines(Object next, final Iterator<?> polylines) { if (!(next instanceof MultiPath || next instanceof Point)) { return null; } @@ -213,15 +214,17 @@ add: for (;;) { } @Override - Polygon createMultiPolygonImpl(Object... polygonsOrLinearRings) { - final Polygon poly = new Polygon(); - for (final Object polr : polygonsOrLinearRings) { - if (polr instanceof MultiPath) { - poly.add((MultiPath) polr, false); - } else throw new UnsupportedOperationException("Unsupported geometry type: "+polr == null ? "null" : polr.getClass().getCanonicalName()); - } + public Polygon createMultiPolygon(Stream<?> polygonsOrLinearRings) { + return polygonsOrLinearRings.map(ESRI::toMultiPath).reduce( + new Polygon(), + (p, m) -> {p.add(m, false); return p;}, + (p1, p2) -> {p1.add(p2, false); return p1;} + ); + } - return poly; + private static MultiPath toMultiPath(Object polr) { + if (polr instanceof MultiPath) return (MultiPath) polr; + else throw new UnsupportedOperationException("Unsupported geometry type: "+polr == null ? "null" : polr.getClass().getCanonicalName()); } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java index 9a17aed..46881bf 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.LogRecord; +import java.util.stream.Stream; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; @@ -335,7 +336,7 @@ public abstract class Geometries<G> { * @return the merged polyline, or {@code null} if the first instance is not an implementation of this library. * @throws ClassCastException if an element in the iterator is not an implementation of this library. */ - abstract G tryMergePolylines(Object first, Iterator<?> polylines); + public abstract G tryMergePolylines(Object first, Iterator<?> polylines); /** * Merges a sequence of points or polylines into a single polyline instances. @@ -455,7 +456,7 @@ public abstract class Geometries<G> { maxY = splittedLeft[3]; Vector[] points2 = clockwiseRing(minX, minY, maxX, maxY); final G secondRect = createPolyline(2, points2); - return createMultiPolygonImpl(mainRect, secondRect); + return createMultiPolygon(Stream.of(mainRect, secondRect)); } /* Geotk original method had an option to insert a median point on wrappped around axis, but we have not ported @@ -498,9 +499,20 @@ public abstract class Geometries<G> { public abstract double[] getPoints(Object geometry); - abstract G createMultiPolygonImpl(final Object... polygonsOrLinearRings); + public abstract G createMultiPolygon(final Stream<?> polygonsOrLinearRings); - public static Object createMultiPolygon(final Object... polygonsOrLinearRings) { - return findStrategy(g -> g.createMultiPolygonImpl(polygonsOrLinearRings)); + public static Object createMultiPolygon_(final Stream polygonsOrLinearRings) { + return findStrategy(g -> g.createMultiPolygon(polygonsOrLinearRings)); + } + + /** + * Try and associate given coordinate reference system to the specified geometry. It should replace any previously + * set referencing information. + * + * @param target The geometry to embed referencing information into. + * @param toApply Referencing information to add. + */ + public void setCRS(G target, CoordinateReferenceSystem toApply) { + throw new UnsupportedOperationException("Not supported yet"); } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java index 28e0fbe..ea6d1f9 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.stream.Stream; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.util.FactoryException; @@ -207,8 +208,8 @@ final class JTS extends Geometries<Geometry> { } @Override - public Geometry toPolygon(Geometry polyline) throws IllegalArgumentException { - if (polyline instanceof Polygon) return polyline; + public Polygon toPolygon(Geometry polyline) throws IllegalArgumentException { + if (polyline instanceof Polygon) return (Polygon) polyline; Polygon result = null; if (polyline instanceof LinearRing) { @@ -269,7 +270,7 @@ final class JTS extends Geometries<Geometry> { * @throws ClassCastException if an element in the iterator is not a JTS geometry. */ @Override - final Geometry tryMergePolylines(Object next, final Iterator<?> polylines) { + public final Geometry tryMergePolylines(Object next, final Iterator<?> polylines) { if (!(next instanceof MultiLineString || next instanceof LineString || next instanceof Point)) { return null; } @@ -332,16 +333,22 @@ add: for (;;) { } @Override - MultiPolygon createMultiPolygonImpl(Object... polygonsOrLinearRings) { - final Polygon[] polys = new Polygon[polygonsOrLinearRings.length]; - for (int i = 0 ; i < polys.length ; i++) { - Object o = polygonsOrLinearRings[i]; - if (o instanceof GeometryWrapper) o = ((GeometryWrapper) o).geometry; + public MultiPolygon createMultiPolygon(Stream<?> polygonsOrLinearRings) { + final Polygon[] polys = polygonsOrLinearRings + .map(this::castToPolygon) + .toArray(size -> new Polygon[size]); + return factory.createMultiPolygon(polys); + } - if (o instanceof Polygon) polys[i] = (Polygon) o; - else if (o instanceof LinearRing) polys[i] = factory.createPolygon((LinearRing) o); - } + private Polygon castToPolygon(Object input) { + if (input instanceof GeometryWrapper) input = ((GeometryWrapper) input).geometry; - return factory.createMultiPolygon(polys); + if (input instanceof Geometry) return toPolygon((Geometry) input); + else throw new IllegalArgumentException("Given argument cannot be cast to polygon"); + } + + @Override + public void setCRS(Geometry target, CoordinateReferenceSystem toApply) { + org.apache.sis.internal.feature.jts.JTS.setCoordinateReferenceSystem(target, toApply); } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java index 7f03643..50a492b 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java @@ -27,6 +27,7 @@ import java.util.Iterator; import java.util.Spliterator; import java.util.function.Consumer; import java.util.stream.DoubleStream; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.apache.sis.geometry.GeneralEnvelope; @@ -37,7 +38,6 @@ import org.apache.sis.setup.GeometryLibrary; import org.apache.sis.util.Classes; import org.apache.sis.util.Numbers; -import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; @@ -201,7 +201,7 @@ final class Java2D extends Geometries<Shape> { * @throws ClassCastException if an element in the iterator is not a {@link Shape} or a {@link Point2D}. */ @Override - final Shape tryMergePolylines(Object next, final Iterator<?> polylines) { + public final Shape tryMergePolylines(Object next, final Iterator<?> polylines) { if (!(next instanceof Shape || next instanceof Point2D)) { return null; } @@ -257,12 +257,10 @@ add: for (;;) { } @Override - Shape createMultiPolygonImpl(Object... polygonsOrLinearRings) { - ensureNonEmpty("Polygons or linear rings to merge", polygonsOrLinearRings); - if (polygonsOrLinearRings.length == 1 && polygonsOrLinearRings[0] instanceof Shape) - return (Shape) polygonsOrLinearRings[0]; - final Iterator<Object> it = Arrays.asList(polygonsOrLinearRings).iterator(); - return tryMergePolylines(it.next(), it); + public Shape createMultiPolygon(Stream<?> polygonsOrLinearRings) { + final Iterator<?> it = polygonsOrLinearRings.iterator(); + if (it.hasNext()) return tryMergePolylines(it.next(), it); + throw new IllegalArgumentException("Empty input"); } @Override diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java index d3e1174..39a9ffc 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java @@ -117,7 +117,7 @@ public final class JTS extends Static { return Optional.empty(); } else if (ud instanceof CoordinateReferenceSystem) { target.setUserData(toSet); - return Optional.of((CoordinateReferenceSystem)ud); + return Optional.of((CoordinateReferenceSystem) ud); } else if (ud instanceof Map) { final Map asMap = (Map) ud; // In case user-data contains other useful data, we don't switch from map to CRS. We also reset SRID. diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java index 943df9e..76bfec0 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java @@ -184,7 +184,7 @@ public class ANSIInterpreter implements FilterVisitor, ExpressionVisitor { public Object visit(BBOX filter, Object extraData) { // TODO: This is a wrong interpretation, but sqlmm has no equivalent of filter encoding bbox, so we'll // fallback on a standard intersection. However, PostGIS, H2, etc. have their own versions of such filters. - return function("ST_Intersects(", filter, extraData); + return function("ST_Intersects", filter, extraData); } @Override diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java index f430e40..ed1c429 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java @@ -14,8 +14,6 @@ import java.time.OffsetTime; import java.time.ZoneOffset; import java.util.Optional; -import org.apache.sis.internal.metadata.sql.Dialect; - public class ANSIMapping implements DialectMapping { /** @@ -30,17 +28,20 @@ public class ANSIMapping implements DialectMapping { } @Override - public Dialect getDialect() { - return Dialect.ANSI; + public Spi getSpi() { + return null; } @Override - public Optional<ColumnAdapter<?>> getMapping(int sqlType, String sqlTypeName) { - return Optional.ofNullable(getMappingImpl(sqlType, sqlTypeName)); + public void close() throws SQLException {} + + @Override + public Optional<ColumnAdapter<?>> getMapping(SQLColumn columnDefinition) { + return Optional.ofNullable(getMappingImpl(columnDefinition)); } - public ColumnAdapter<?> getMappingImpl(int sqlType, String sqlTypeName) { - switch (sqlType) { + public ColumnAdapter<?> getMappingImpl(SQLColumn columnDefinition) { + switch (columnDefinition.type) { case Types.BIT: case Types.BOOLEAN: return forceCast(Boolean.class); case Types.TINYINT: if (!isByteUnsigned) return forceCast(Byte.class); // else fallthrough. @@ -54,18 +55,18 @@ public class ANSIMapping implements DialectMapping { case Types.DECIMAL: return forceCast(BigDecimal.class); case Types.CHAR: case Types.VARCHAR: - case Types.LONGVARCHAR: return new ColumnAdapter<>(String.class, ResultSet::getString); - case Types.DATE: return new ColumnAdapter<>(Date.class, ResultSet::getDate); - case Types.TIME: return new ColumnAdapter<>(LocalTime.class, ANSIMapping::toLocalTime); - case Types.TIMESTAMP: return new ColumnAdapter<>(Instant.class, ANSIMapping::toInstant); - case Types.TIME_WITH_TIMEZONE: return new ColumnAdapter<>(OffsetTime.class, ANSIMapping::toOffsetTime); - case Types.TIMESTAMP_WITH_TIMEZONE: return new ColumnAdapter<>(OffsetDateTime.class, ANSIMapping::toODT); + case Types.LONGVARCHAR: return new ColumnAdapter.Simple<>(String.class, ResultSet::getString); + case Types.DATE: return new ColumnAdapter.Simple<>(Date.class, ResultSet::getDate); + case Types.TIME: return new ColumnAdapter.Simple<>(LocalTime.class, ANSIMapping::toLocalTime); + case Types.TIMESTAMP: return new ColumnAdapter.Simple<>(Instant.class, ANSIMapping::toInstant); + case Types.TIME_WITH_TIMEZONE: return new ColumnAdapter.Simple<>(OffsetTime.class, ANSIMapping::toOffsetTime); + case Types.TIMESTAMP_WITH_TIMEZONE: return new ColumnAdapter.Simple<>(OffsetDateTime.class, ANSIMapping::toODT); case Types.BINARY: case Types.VARBINARY: - case Types.LONGVARBINARY: return new ColumnAdapter<>(byte[].class, ResultSet::getBytes); + case Types.LONGVARBINARY: return new ColumnAdapter.Simple<>(byte[].class, ResultSet::getBytes); case Types.ARRAY: return forceCast(Object[].class); case Types.OTHER: // Database-specific accessed via getObject and setObject. - case Types.JAVA_OBJECT: return new ColumnAdapter<>(Object.class, ResultSet::getObject); + case Types.JAVA_OBJECT: return new ColumnAdapter.Simple<>(Object.class, ResultSet::getObject); default: return null; } } @@ -95,7 +96,7 @@ public class ANSIMapping implements DialectMapping { } private static <T> ColumnAdapter<T> forceCast(final Class<T> targetType) { - return new ColumnAdapter<>(targetType, (r, i) -> forceCast(targetType, r, i)); + return new ColumnAdapter.Simple<>(targetType, (r, i) -> forceCast(targetType, r, i)); } private static <T> T forceCast(final Class<T> targetType, ResultSet source, final Integer columnIndex) throws SQLException { diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java index 4a9cdd1..e58d0c0 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java @@ -16,6 +16,7 @@ */ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -27,8 +28,6 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import javax.sql.DataSource; -import org.opengis.feature.Feature; -import org.opengis.feature.PropertyType; import org.opengis.util.GenericName; import org.opengis.util.NameFactory; import org.opengis.util.NameSpace; @@ -52,6 +51,8 @@ import org.apache.sis.util.iso.Names; import org.apache.sis.util.logging.WarningListeners; import org.apache.sis.util.resources.ResourceInternationalString; +import static org.apache.sis.util.ArgumentChecks.ensureNonNull; + /** * Helper methods for creating {@code FeatureType}s from database structure. @@ -60,6 +61,7 @@ import org.apache.sis.util.resources.ResourceInternationalString; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) + * @author Alexis Manin (Geomatys) * @version 1.0 * @since 1.0 * @module @@ -73,6 +75,11 @@ final class Analyzer { final DataSource source; /** + * A connection used all along this component life to query database. + */ + final Connection connection; + + /** * Information about the database as a whole. * Used for fetching tables, columns, primary keys <i>etc.</i> */ @@ -147,20 +154,24 @@ final class Analyzer { * Creates a new analyzer for the database described by given metadata. * * @param source the data source, usually given by user at {@code SQLStore} creation time. - * @param metadata Value of {@code source.getConnection().getMetaData()}. + * @param databaseConnection Database entrypoint. It's the caller responsability to handle connection lifecycle, + * and ensure this object life span is shorter than the connection one. * @param listeners Value of {@code SQLStore.listeners}. * @param locale Value of {@code SQLStore.getLocale()}. */ - Analyzer(final DataSource source, final DatabaseMetaData metadata, final WarningListeners<DataStore> listeners, + Analyzer(final DataSource source, final Connection databaseConnection, final WarningListeners<DataStore> listeners, final Locale locale) throws SQLException { + ensureNonNull("Database connection provider", source); + ensureNonNull("Database connection", databaseConnection); this.source = source; - this.metadata = metadata; + this.connection = databaseConnection; + this.metadata = databaseConnection.getMetaData(); this.listeners = listeners; this.locale = locale; this.strings = new HashMap<>(); this.escape = metadata.getSearchStringEscape(); - this.functions = new SpatialFunctions(metadata); + this.functions = new SpatialFunctions(databaseConnection, metadata); this.nameFactory = DefaultFactories.forBuildin(NameFactory.class); /* * The following tables are defined by ISO 19125 / OGC Simple feature access part 2. @@ -305,10 +316,6 @@ final class Analyzer { warnings.add(Resources.formatInternational(key, argument)); } - private PropertyAdapter analyze(SQLColumn target) { - throw new UnsupportedOperationException(); - } - /** * Invoked after we finished to create all tables. This method flush the warnings * (omitting duplicated warnings), then returns all tables including dependencies. @@ -345,26 +352,21 @@ final class Analyzer { int i = 0; for (SQLColumn col : spec.getColumns()) { i++; - final ColumnAdapter<?> colAdapter = functions.toJavaType(col.getType(), col.getTypeName()); - Class<?> type = colAdapter.javaType; - final String colName = col.getName().getColumnName(); - final String attrName = col.getName().getAttributeName(); - if (type == null) { - warning(Resources.Keys.UnknownType_1, colName); - type = Object.class; - } + final ColumnAdapter<?> colAdapter = functions.toJavaType(col); + Class<?> type = colAdapter.getJavaType(); + final String colName = col.naming.getColumnName(); + final String attrName = col.naming.getAttributeName(); final AttributeTypeBuilder<?> attribute = builder .addAttribute(type) .setName(attrName); - if (col.isNullable()) attribute.setMinimumOccurs(0); - final int precision = col.getPrecision(); - /* TODO: we should check column type. Precision for numbers or blobs is meaningfull, but the convention + if (col.isNullable) attribute.setMinimumOccurs(0); + /* TODO: we should check column type. Precision for numbers or blobs is meaningful, but the convention * exposed by SIS does not allow to distinguish such cases. */ - if (precision > 0) attribute.setMaximalLength(precision); + if (col.precision > 0) attribute.setMaximalLength(col.precision); - col.getCrs().ifPresent(attribute::setCRS); + colAdapter.getCrs().ifPresent(attribute::setCRS); if (geomCol.equals(attrName)) attribute.addRole(AttributeRole.DEFAULT_GEOMETRY); if (pkCols.contains(colName)) attribute.addRole(AttributeRole.IDENTIFIER_COMPONENT); @@ -422,11 +424,10 @@ final class Analyzer { } } - private interface PropertyAdapter { - PropertyType getType(); - void fill(ResultSet source, final Feature target); - } - + /** + * TODO: this object needs a live connection. Check if we should parse all information at built, to avoid requiring + * keeping a connection all along. + */ private final class TableMetadata implements SQLTypeSpecification { final TableReference id; private final String tableEsc; @@ -448,7 +449,6 @@ final class Analyzer { final List<String> cols = new ArrayList<>(); while (reflect.next()) { cols.add(getUniqueString(reflect, Reflection.COLUMN_NAME)); - // The actual Boolean value will be fetched in the loop on columns later. } pk = PrimaryKey.create(cols); } @@ -577,12 +577,18 @@ final class Analyzer { final ArrayList<SQLColumn> tmpCols = new ArrayList<>(total); for (int i = 1 ; i <= total ; i++) { + final TableReference optTable; + final String table = meta.getTableName(i); + if (table != null) { + optTable = new TableReference(meta.getCatalogName(i), meta.getSchemaName(i), table, null); + } else optTable = null; tmpCols.add(new SQLColumn( meta.getColumnType(i), meta.getColumnTypeName(i), meta.isNullable(i) == ResultSetMetaData.columnNullable, new ColumnRef(meta.getColumnName(i)).as(meta.getColumnLabel(i)), - meta.getPrecision(i) + meta.getPrecision(i), + optTable )); } @@ -619,5 +625,4 @@ final class Analyzer { return Collections.EMPTY_LIST; } } - } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/CRSIdentification.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/CRSIdentification.java new file mode 100644 index 0000000..a329184 --- /dev/null +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/CRSIdentification.java @@ -0,0 +1,91 @@ +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.text.ParseException; + +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.util.FactoryException; + +import org.apache.sis.io.wkt.Convention; +import org.apache.sis.io.wkt.WKTFormat; +import org.apache.sis.referencing.CRS; +import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.util.collection.Cache; + +final class CRSIdentification implements SQLCloseable { + + final PreparedStatement wktFromSrid; + private final WKTFormat wktReader; + + private final Cache<Integer, CoordinateReferenceSystem> sessionCache; + + CRSIdentification(final Connection c, final Cache<Integer, CoordinateReferenceSystem> sessionCache) throws SQLException { + wktFromSrid = c.prepareStatement("SELECT auth_name, auth_srid, srtext FROM spatial_ref_sys WHERE srid=?"); + wktReader = new WKTFormat(null, null); + wktReader.setConvention(Convention.WKT1_COMMON_UNITS); + this.sessionCache = sessionCache; + } + + /** + * Try to fetch spatial system relative to given SRID. + * + * @param pgSrid The SRID as defined by the database (see + * <a href="http://postgis.refractions.net/documentation/manual-1.3/ch04.html#id2571265">Official PostGIS documentation</a> for details). + * @return If input was 0 or less, a null value is returned. Otherwise, return the CRS decoded from database WKT. + * @throws IllegalArgumentException If given SRID is above 0, but no coordinate system definition can be found for + * it in the database, or found object is not a database, or no WKT is available, but authority code is not + * supported by SIS. + * @throws IllegalStateException If more than one match is found for given SRID. + */ + CoordinateReferenceSystem fetchCrs(int pgSrid) throws IllegalArgumentException { + if (pgSrid <= 0) return null; + + return sessionCache.computeIfAbsent(pgSrid, this::fetch); + } + + private CoordinateReferenceSystem fetch(final int pgSrid) { + try { + wktFromSrid.setInt(1, pgSrid); + try (ResultSet result = wktFromSrid.executeQuery()) { + if (!result.next()) throw new IllegalArgumentException("No entry found for SRID " + pgSrid); + final String authority = result.getString(1); + final int authorityCode = result.getInt(2); + final String pgWkt = result.getString(3); + + // That should never happen, but if it does, there's a serious problem ! + if (result.next()) + throw new IllegalStateException("More than one definition available for SRID " + pgSrid); + + if (pgWkt == null || pgWkt.trim().isEmpty()) { + try { + return CRS.getAuthorityFactory(authority).createCoordinateReferenceSystem(Integer.toString(authorityCode)); + } catch (FactoryException e) { + throw new IllegalArgumentException(String.format( + "Input SRID (%d) does not provide any WKT, but its authority code (%s:%d) is not supported by SIS", + pgSrid, authority, authorityCode + ), e); + } + } + final Object parsedWkt = wktReader.parseObject(pgWkt); + if (parsedWkt instanceof CoordinateReferenceSystem) { + return (CoordinateReferenceSystem) parsedWkt; + } else throw new ParseException(String.format( + "WKT of given SRID cannot be interprated as a CRS.%nInput SRID: %d%nOutput type: %s", + pgSrid, parsedWkt.getClass().getCanonicalName() + ), 0); + } finally { + wktFromSrid.clearParameters(); + } + } catch (SQLException | ParseException e) { + throw new BackingStoreException(e); + } + } + + @Override + public void close() throws SQLException { + wktFromSrid.close(); + } +} 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 8fdd3a5..4d5602d 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 @@ -1,8 +1,10 @@ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.function.Function; +import java.util.Optional; + +import org.opengis.referencing.crs.CoordinateReferenceSystem; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; @@ -12,24 +14,51 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull; * * @param <T> Type of object decoded from cell. */ -class ColumnAdapter<T> implements SQLBiFunction<ResultSet, Integer, T> { - final Class<T> javaType; - private final SQLBiFunction<ResultSet, Integer, T> fetchValue; - - protected ColumnAdapter(Class<T> javaType, SQLBiFunction<ResultSet, Integer, T> fetchValue) { - ensureNonNull("Result java type", javaType); - ensureNonNull("Function for value retrieval", fetchValue); - this.javaType = javaType; - this.fetchValue = fetchValue; - } +public interface ColumnAdapter<T> { - @Override - public T apply(ResultSet resultSet, Integer integer) throws SQLException { - return fetchValue.apply(resultSet, integer); + /** + * Gives a function ready to extract and interpret values of a result set for the column it has been designed for. + * + * @param target A read-only connection that can be used to load metadata and stuff related to target column. + * @return A function which will interpret values for the column this component has been created for. User will have + * to give it a well-positioned cursor (result set on the wanted line) as the index of the cell it must read on it. + */ + SQLBiFunction<ResultSet, Integer, T> prepare(final Connection target); + + /** + * Note : This method could be used not only for geometric fields, but also on numeric ones representing 1D systems. + * + * @return Potentially an empty shell, or the default coordinate reference system for this column values. + */ + default Optional<CoordinateReferenceSystem> getCrs() { + return Optional.empty(); } - @Override - public <V> SQLBiFunction<ResultSet, Integer, V> andThen(Function<? super T, ? extends V> after) { - return fetchValue.andThen(after); + /** + * + * @return The (possibly parent) type of objects read by this mapper. Note that it MUST NOT return null values. + */ + Class<T> getJavaType(); + + final class Simple<T> implements ColumnAdapter<T> { + private final Class<T> javaType; + private final SQLBiFunction<ResultSet, Integer, T> fetchValue; + + Simple(final Class<T> targetType, SQLBiFunction<ResultSet, Integer, T> fetchValue) { + ensureNonNull("Target type", targetType); + ensureNonNull("Function for value retrieval", fetchValue); + javaType = targetType; + this.fetchValue = fetchValue; + } + + @Override + public SQLBiFunction<ResultSet, Integer, T> prepare(Connection target) { + return fetchValue; + } + + @Override + public Class<T> getJavaType() { + return javaType; + } } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java index 3f04da4..0985cfb 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java @@ -112,7 +112,7 @@ public final class Database { final GenericName[] tableNames, final WarningListeners<DataStore> listeners) throws SQLException, DataStoreException { - final Analyzer analyzer = new Analyzer(source, connection.getMetaData(), listeners, store.getLocale()); + final Analyzer analyzer = new Analyzer(source, connection, listeners, store.getLocale()); final String[] tableTypes = getTableTypes(analyzer.metadata); final Set<TableReference> declared = new LinkedHashSet<>(); for (final GenericName tableName : tableNames) { @@ -233,4 +233,32 @@ public final class Database { public String toString() { return TableReference.toString(this, (n) -> appendTo(n)); } + + /** + * Acquire a connection over parent database, forcing a few parameters to ensure optimal read performance and + * limiting user rights : + * <ul> + * <li>{@link Connection#setAutoCommit(boolean) auto-commit} to false</li> + * <li>{@link Connection#setReadOnly(boolean) querying read-only}</li> + * </ul> + * + * @param source Database pointer to create connection from. + * @return A new connection to database, with deactivated auto-commit. + * @throws SQLException If we cannot create a new connection. See {@link DataSource#getConnection()} for details. + */ + public static Connection connectReadOnly(final DataSource source) throws SQLException { + final Connection c = source.getConnection(); + try { + c.setAutoCommit(false); + c.setReadOnly(true); + } catch (SQLException e) { + try { + c.close(); + } catch (RuntimeException | SQLException bis) { + e.addSuppressed(bis); + } + throw e; + } + return c; + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/DialectMapping.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/DialectMapping.java index ce6e403..33d0266 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/DialectMapping.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/DialectMapping.java @@ -1,12 +1,27 @@ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; +import java.sql.SQLException; import java.util.Optional; import org.apache.sis.internal.metadata.sql.Dialect; -public interface DialectMapping { +public interface DialectMapping extends SQLCloseable { - Dialect getDialect(); + Spi getSpi(); - Optional<ColumnAdapter<?>> getMapping(final int sqlType, final String sqlTypeName); + Optional<ColumnAdapter<?>> getMapping(final SQLColumn columnDefinition); + + interface Spi { + /** + * + * @param c The connection to use to connect to the database. It will be read-only. + * @return A component compatible with database of given connection, or nothing if the database is not supported + * by this component. + * @throws SQLException If an error occurs while fetching information from database. + */ + Optional<DialectMapping> create(final Connection c) throws SQLException; + + Dialect getDialect(); + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java new file mode 100644 index 0000000..8f4bc1f --- /dev/null +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java @@ -0,0 +1,261 @@ +package org.apache.sis.internal.sql.feature; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Iterator; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.stream.IntStream; + +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +import org.apache.sis.internal.feature.Geometries; +import org.apache.sis.math.Vector; +import org.apache.sis.setup.GeometryLibrary; + +import static java.lang.Character.digit; +import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; + +/** + * PostGIS EWKB Geometry reader/write classes. + * http://postgis.net/docs/using_postgis_dbmanagement.html#EWKB_EWKT + * + * This format is the natural form returned by a query selection a geometry field + * whithout using any ST_X method. + * + * TODO: This format is almost equivalent to standard WKB, except that it includes SRID information. If needed, adding + * minor tweaks and flags should suffice to make it compatible with both formats. See {@link #MASK_SRID } usage for + * details. + * + * @author Johann Sorel (Geomatys) + * @author Alexis Manin (Geomatys) + */ +class EWKBReader { + + private static final int MASK_Z = 0x80000000; + private static final int MASK_M = 0x40000000; + private static final int MASK_SRID = 0x20000000; + private static final int MASK_GEOMTYPE = 0x1FFFFFFF; + + // Copied from PostGIS JDBC source code to avoid dependency for too little information. + private static final int POINT = 1; + private static final int LINESTRING = 2; + private static final int POLYGON = 3; + private static final int MULTIPOINT = 4; + private static final int MULTILINESTRING = 5; + private static final int MULTIPOLYGON = 6; + private static final int GEOMETRYCOLLECTION = 7; + + final Geometries factory; + + private final Function<ByteBuffer, ?> decoder; + + EWKBReader() { + this((GeometryLibrary) null); + } + + EWKBReader(GeometryLibrary library) { + this(Geometries.implementation(library)); + } + + EWKBReader(Geometries geometryFactory) { + this(geometryFactory, bytes -> new Reader(geometryFactory, bytes).read()); + } + + private EWKBReader(final Geometries factory, Function<ByteBuffer, ?> decoder) { + this.factory = factory; + this.decoder = decoder; + } + + /** + * + * @param defaultCrs The coordinate reference system to associate to each geometry. + * @return A NEW instance of reader, with a fixed CRS resolution, applying constant value to all read geometries. + */ + public EWKBReader forCrs(CoordinateReferenceSystem defaultCrs) { + if (defaultCrs == null) return new EWKBReader(factory); + else return new EWKBReader(factory, bytes -> { + final Object geom = new Reader(factory, bytes).read(); + if (geom != null) factory.setCRS(geom, defaultCrs); + return geom; + }); + } + + public EWKBReader withResolver(final IntFunction<CoordinateReferenceSystem> fromPgSridToCrs) { + return new EWKBReader(factory, bytes -> { + final Reader reader = new Reader(factory, bytes); + final Object geom = reader.read(); + if (reader.srid > 0) { + final CoordinateReferenceSystem crs = fromPgSridToCrs.apply(reader.srid); + if (crs != null) factory.setCRS(geom, crs); + } + return geom; + }); + } + + Object readHexa(final String hexaEWkb) { + return read(decodeHex(hexaEWkb)); + } + + Object read(final byte[] eWkb) { + return decoder.apply(ByteBuffer.wrap(eWkb)); + } + + Object read(final ByteBuffer eWkb) { + return decoder.apply(eWkb); + } + + private static final class Reader { + final Geometries factory; + final ByteBuffer buffer; + final int geomType; + final int dimension; + final int srid; + + private Reader(Geometries factory, ByteBuffer buffer) { + this.factory = factory; + final byte endianess = buffer.get(); + if (isLittleEndian(endianess)) { + this.buffer = buffer.order(ByteOrder.LITTLE_ENDIAN); + } else this.buffer = buffer; + final int flags = buffer.getInt(); + final boolean flagZ = (flags & MASK_Z) != 0; + final boolean flagM = (flags & MASK_M) != 0; + final boolean flagSRID = (flags & MASK_SRID) != 0; + geomType = (flags & MASK_GEOMTYPE); + dimension = 2 + ((flagZ) ? 1 : 0) + ((flagM) ? 1 : 0); + srid = flagSRID ? buffer.getInt() : 0; + } + + Object read() { + switch (geomType) { + case POINT: return readPoint(); + case LINESTRING: return readLineString(); + case POLYGON: return readPolygon(); + case MULTIPOINT: return readMultiPoint(); + case MULTILINESTRING: return readMultiLineString(); + case MULTIPOLYGON: return readMultiPolygon(); + case GEOMETRYCOLLECTION: return readCollection(); + } + + throw new UnsupportedOperationException("Unsupported geometry type: "+geomType); + } + + private Object readMultiLineString() { + final Iterator<Object> it = IntStream.range(0, readCount()) + .mapToObj(i -> new Reader(factory, buffer).read()) + .iterator(); + if (it.hasNext()) { + final Object first = it.next(); + if (it.hasNext()) return factory.tryMergePolylines(first, it); + else return first; + } + throw new IllegalStateException("No geometry decoded"); + } + + private Object readMultiPolygon() { + final int count = readCount(); + final Object[] polygons = new Object[count]; + for (int i = 0 ; i < count ; i++) { + polygons[i] = new Reader(factory, buffer).read(); + } + + return factory.createMultiPolygon( + IntStream.range(0, readCount()) + .mapToObj(i -> new Reader(factory, buffer).read()) + ); + } + + private Object readCollection() { + throw new UnsupportedOperationException(); + } + + final Object readPoint() { + final double[] ordinates = readCoordinateSequence(1); + // TODO: we lose information here ! We need to evolve Geometry API. + return factory.createPoint(ordinates[0], ordinates[1]); + } + + final Object readLineString() { + final double[] ordinates = readCoordinateSequence(); + return factory.createPolyline(dimension, Vector.create(ordinates)); + } + + private Object readPolygon() { + final int nbRings=buffer.getInt(); + final Vector outerShell = Vector.create(readCoordinateSequence()); + + final double[] nans = new double[dimension]; + Arrays.fill(nans, Double.NaN); + final Vector separator = Vector.create(nans); + final Vector[] allShells = new Vector[Math.addExact(nbRings, nbRings -1)]; // include ring separators + allShells[0] = outerShell; + for (int i = 1 ; i < nbRings ;) { + allShells[i++] = separator; + allShells[i++] = Vector.create(readCoordinateSequence()); + } + + return factory.toPolygon(factory.createPolyline(dimension, outerShell)); + } + + private double[] readMultiPoint() { + throw new UnsupportedOperationException(); + } + + private double[] readCoordinateSequence() { + return readCoordinateSequence(readCount()); + } + + private double[] readCoordinateSequence(int nbPts) { + final double[] brutPoint = new double[Math.multiplyExact(dimension, nbPts)]; + for (int i = 0 ; i < brutPoint.length ; i++) brutPoint[i] = buffer.getDouble(); + return brutPoint; + } + + /** + * @implNote WKB specification declares lengths as uint32. However, the way to handle it in Java would be to + * return a long value, which is not possible anyway, because current implementation needs to put all geometry + * data in memory, in an array whose number of elements is limited to {@link Integer#MAX_VALUE}. So for now, + * we will just ensure that read value is compatible with our limitations. + * + * For details, see <a href="https://www.ibm.com/support/knowledgecenter/SSGU8G_14.1.0/com.ibm.spatial.doc/ids_spat_285.htm">IBM</a> + * or <a href="https://trac.osgeo.org/postgis/browser/trunk/doc/ZMSgeoms.txt">OSGEO</a> documentation. + * + * @return read count. + * @throws IllegalStateException If read count is 0 or above {@link Integer#MAX_VALUE}. + */ + private int readCount() { + final int count = buffer.getInt(); + if (count == 0) throw new IllegalStateException("Read a 0 point/geometry count in WKB."); + else if (count < 0) throw new IllegalStateException("Read a count overflowing Java integer max value: "+Integer.toUnsignedLong(count)); + return count; + } + } + + private static boolean isLittleEndian(byte endianess) { + return endianess == 1; // org.postgis.binary.ValueGetter.NDR.NUMBER + } + + /** + * Convert a text representing an hexadecimal set of values (no separator, each 2 characters form a value). + * + * @param hexa The hexadecimal values to decode. Should neither be null nor empty. + * @return Real values encoded by input hexadecimal text. Never null, never empty. + */ + static byte[] decodeHex(String hexa) { + ensureNonEmpty("Hexadecimal text", hexa); + int len = hexa.length(); + // Handle odd length by considering last character as a lone value + byte[] data = new byte[(len+1) / 2]; + int limit = (len % 2 == 0) ? len : len - 1; + for (int i = 0, j=0 ; i < limit ; ) { + data[j++] = (byte) ((digit(hexa.charAt(i++), 16) << 4) + + digit(hexa.charAt(i++), 16)); + } + + if (limit < len) data[data.length - 1] = (byte) digit(hexa.charAt(limit), 16); + + return data; + } +} 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 88c7546..d9a2752 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 @@ -6,6 +6,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; @@ -25,40 +26,57 @@ class FeatureAdapter { this.attributeMappers = Collections.unmodifiableList(new ArrayList<>(attributeMappers)); } - Feature read(final ResultSet cursor, final Connection origin) throws SQLException { - final Feature result = readAttributes(cursor); - addImports(result, cursor); - addExports(result); - return result; + ResultSetAdapter prepare(final Connection target) { + final List<ReadyMapper> rtu = attributeMappers.stream() + .map(mapper -> mapper.prepare(target)) + .collect(Collectors.toList()); + return new ResultSetAdapter(rtu); } - private void addImports(final Feature target, final ResultSet cursor) { - // TODO: see Features class - } + final class ResultSetAdapter { + final List<ReadyMapper> mappers; - private void addExports(final Feature target) { - // TODO: see Features class - } + ResultSetAdapter(List<ReadyMapper> mappers) { + this.mappers = mappers; + } - private Feature readAttributes(final ResultSet cursor) throws SQLException { - final Feature result = type.newInstance(); - for (PropertyMapper mapper : attributeMappers) mapper.read(cursor, result); - return result; - } + Feature read(final ResultSet cursor) throws SQLException { + final Feature result = readAttributes(cursor); + addImports(result, cursor); + addExports(result); + return result; + } - List<Feature> prefetch(final int size, final ResultSet cursor, final Connection origin) throws SQLException { - // TODO: optimize by resolving import associations by batch import fetching. - final ArrayList<Feature> features = new ArrayList<>(size); - for (int i = 0 ; i < size && cursor.next() ; i++) { - features.add(read(cursor, origin)); + private Feature readAttributes(final ResultSet cursor) throws SQLException { + final Feature result = type.newInstance(); + for (ReadyMapper mapper : mappers) mapper.read(cursor, result); + return result; } - return features; + //final SQLBiFunction<ResultSet, Integer, ?>[] adapters; + List<Feature> prefetch(final int size, final ResultSet cursor) throws SQLException { + // TODO: optimize by resolving import associations by batch import fetching. + final ArrayList<Feature> features = new ArrayList<>(size); + for (int i = 0 ; i < size && cursor.next() ; i++) { + features.add(read(cursor)); + } + + return features; + } + + private void addImports(final Feature target, final ResultSet cursor) { + // TODO: see Features class + } + + private void addExports(final Feature target) { + // TODO: see Features class + } } static final class PropertyMapper { // TODO: by using a indexed implementation of Feature, we could avoid the name mapping. However, a JMH benchmark - // would be required in order to be sure it's impacting performance positively. + // would be required in order to be sure it's impacting performance positively. also, features are sparse by + // nature, and an indexed implementation could (to verify, still) be bad on memory footprint. final String propertyName; final int columnIndex; final ColumnAdapter fetchValue; @@ -69,9 +87,23 @@ class FeatureAdapter { this.fetchValue = fetchValue; } + ReadyMapper prepare(final Connection target) { + return new ReadyMapper(this, fetchValue.prepare(target)); + } + } + + private static class ReadyMapper { + final SQLBiFunction<ResultSet, Integer, ?> reader; + final PropertyMapper parent; + + public ReadyMapper(PropertyMapper parent, SQLBiFunction<ResultSet, Integer, ?> reader) { + this.reader = reader; + this.parent = parent; + } + private void read(ResultSet cursor, Feature target) throws SQLException { - final Object value = fetchValue.apply(cursor, columnIndex); - if (value != null) target.setPropertyValue(propertyName, value); + final Object value = reader.apply(cursor, parent.columnIndex); + if (value != null) target.setPropertyValue(parent.propertyName, value); } } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java index dd97df9..1bbc3f0 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Features.java @@ -151,7 +151,7 @@ final class Features implements Spliterator<Feature> { */ private final long estimatedSize; - private final FeatureAdapter adapter; + private final FeatureAdapter.ResultSetAdapter adapter; /** * Creates a new iterator over the feature instances. @@ -182,7 +182,7 @@ final class Features implements Spliterator<Feature> { attributeNames[i++] = column.getAttributeName(); } this.featureType = table.featureType; - this.adapter = table.adapter; + this.adapter = table.adapter.prepare(connection); final DatabaseMetaData metadata = connection.getMetaData(); estimatedSize = following.isEmpty() ? table.countRows(metadata, true) : 0; /* @@ -418,7 +418,7 @@ final class Features implements Spliterator<Feature> { private boolean fetch(final Consumer<? super Feature> action, final boolean all) throws SQLException { while (result.next()) { // TODO: give connection to adapter. - final Feature feature = adapter.read(result, null); + final Feature feature = adapter.read(result); for (int i=0; i < dependencies.length; i++) { final Features dependency = dependencies[i]; final int[] columnIndices = foreignerKeyIndices[i]; diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java new file mode 100644 index 0000000..bb5c67e --- /dev/null +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryIdentification.java @@ -0,0 +1,159 @@ +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.text.ParseException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +import org.apache.sis.util.collection.Cache; + +import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; + +/** + * Not THREAD-SAFE ! + * + * @implNote <a href="https://www.jooq.org/doc/3.12/manual/sql-execution/fetching/pojos/#N5EFC1">I miss JOOQ...</a> + */ +class GeometryIdentification implements SQLCloseable { + + final PreparedStatement identifySchemaQuery; + /** + * A statement serving two purposes: + * <ol> + * <li>Searching all available geometric columns of a specified table</li> + * <li>Fetching geometric information for a specific column</li> + * </ol> + * + * @implNote The statement definition is able to serve both purposes by changing geometry column filter to no-op. + */ + final PreparedStatement columnQuery; + final CRSIdentification crsIdent; + + /** + * Describes if geometry column registry include a column for geometry types, according that one can apparently omit + * it (see Simple_feature_access_-_Part_2_SQL_option_v1.2.1, section 6.2: Architecture - SQL implementation using + * Geometry Types). + */ + final boolean typeIncluded; + + public GeometryIdentification(Connection c, Cache<Integer, CoordinateReferenceSystem> sessionCache) throws SQLException { + this(c, "geometry_columns", "f_geometry_column", "geometry_type", sessionCache); + } + + public GeometryIdentification(Connection c, String identificationTable, String geometryColumnName, String typeColumnName, Cache<Integer, CoordinateReferenceSystem> sessionCache) throws SQLException { + typeIncluded = typeColumnName != null && !(typeColumnName=typeColumnName.trim()).isEmpty(); + identifySchemaQuery = c.prepareStatement("SELECT DISTINCT(f_table_schema) FROM "+identificationTable+" WHERE f_table_name = ?"); + columnQuery = c.prepareStatement( + "SELECT "+geometryColumnName+", coord_dimension, srid" + (typeIncluded ? ", "+typeColumnName : "") + ' ' + + "FROM "+identificationTable+" " + + "WHERE f_table_schema LIKE ? " + + "AND f_table_name = ? " + + "AND "+geometryColumnName+" LIKE ?" + ); + crsIdent = new CRSIdentification(c, sessionCache); + } + + Set<GeometryColumn> fetchGeometricColumns(String schema, final String table) throws SQLException, ParseException { + ensureNonEmpty("Table name", table); + if (schema == null || (schema = schema.trim()).isEmpty()) { + // To avoid ambiguity, we have to restrict search to a single schema + identifySchemaQuery.setString(1, table); + try (ResultSet result = identifySchemaQuery.executeQuery()) { + if (!result.next()) return Collections.EMPTY_SET; + schema = result.getString(1); + if (result.next()) throw new IllegalArgumentException("Multiple tables match given name. Please specify schema to remove all ambiguities"); + } + } + + columnQuery.setString(1, schema); + columnQuery.setString(2, table); + columnQuery.setString(3, "%"); + try (ResultSet result = columnQuery.executeQuery()) { + final HashSet<GeometryColumn> cols = new HashSet<>(); + while (result.next()) { + cols.add(create(result)); + } + return cols; + } finally { + columnQuery.clearParameters(); + } + } + + Optional<GeometryColumn> fetch(SQLColumn target) throws SQLException, ParseException { + if (target == null || target.origin == null) return Optional.empty(); + + String schema = target.origin.schema; + if (schema == null || (schema = schema.trim()).isEmpty()) schema = "%"; + columnQuery.setString(1, schema); + columnQuery.setString(2, target.origin.table); + columnQuery.setString(3, target.naming.getColumnName()); + + try (ResultSet result = columnQuery.executeQuery()) { + if (result.next()) return Optional.of(create(result)); + } finally { + columnQuery.clearParameters(); + } + return Optional.empty(); + } + + private GeometryColumn create(final ResultSet cursor) throws SQLException, ParseException { + final String name = cursor.getString(1); + final int dimension = cursor.getInt(2); + final int pgSrid = cursor.getInt(3); + final String type = cursor.getString(4); + // Note: we make a query per entry, which could impact performance. However, 99% of defined tables + // will have only one geometry column. Moreover, even with more than one, with prepared statement, the + // performance impact should stay low. + final CoordinateReferenceSystem crs = crsIdent.fetchCrs(pgSrid); + return new GeometryColumn(name, dimension, pgSrid, type, crs); + } + + @Override + public void close() throws SQLException { + try ( + SQLCloseable c1 = columnQuery::close; + SQLCloseable c2 = identifySchemaQuery::close; + SQLCloseable c3 = crsIdent + ) {} + } + + static final class GeometryColumn { + final String name; + final int dimension; + final int pgSrid; + final String type; + + final CoordinateReferenceSystem crs; + + private GeometryColumn(String name, int dimension, int srid, final String type, CoordinateReferenceSystem crs) { + this.name = name; + this.dimension = dimension; + this.pgSrid = srid; + this.crs = crs; + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GeometryColumn that = (GeometryColumn) o; + return dimension == that.dimension && + pgSrid == that.pgSrid && + name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} 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 9c9303d..eb3915a 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 @@ -1,29 +1,255 @@ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; import java.sql.Types; +import java.text.ParseException; import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +import org.apache.sis.internal.feature.Geometries; import org.apache.sis.internal.metadata.sql.Dialect; +import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.util.collection.Cache; +import org.apache.sis.util.logging.Logging; + +public final class PostGISMapping implements DialectMapping { + + final PostGISMapping.Spi spi; + final GeometryIdentification identifyGeometries; + final GeometryIdentification identifyGeographies; + + final Connection connection; + + final Geometries library; + + /** + * A cache valid ONLY FOR A DATASOURCE. IT'S IMPORTANT ! Why ? Because : + * <ul> + * <li>CRS definition could differ between databases (PostGIS version, user alterations, etc.)</li> + * <li>Avoid inter-database locking</li> + * </ul> + */ + final Cache<Integer, CoordinateReferenceSystem> sessionCache; + + private PostGISMapping(final PostGISMapping.Spi spi, Connection c) throws SQLException { + connection = c; + this.spi = spi; + sessionCache = new Cache<>(7, 0, true); + this.identifyGeometries = new GeometryIdentification(c, "geometry_columns", "f_geometry_column", "type", sessionCache); + this.identifyGeographies = new GeometryIdentification(c, "geography_columns", "f_geography_column", "type", sessionCache); + + this.library = Geometries.implementation(null); + } -public class PostGISMapping implements DialectMapping { @Override - public Dialect getDialect() { - return Dialect.POSTGRESQL; + public Spi getSpi() { + return spi; } @Override - public Optional<ColumnAdapter<?>> getMapping(int sqlType, String sqlTypeName) { - switch (sqlType) { - case (Types.OTHER): + public Optional<ColumnAdapter<?>> getMapping(SQLColumn definition) { + switch (definition.type) { + case (Types.OTHER): return Optional.ofNullable(forOther(definition)); } return Optional.empty(); } - private ColumnAdapter<?> forOther(String sqlTypeName) { - switch (sqlTypeName.toLowerCase()) { + private ColumnAdapter<?> forOther(SQLColumn definition) { + switch (definition.typeName.trim().toLowerCase()) { case "geometry": + return forGeometry(definition, identifyGeometries); case "geography": + return forGeometry(definition, identifyGeographies); default: return null; } } + + private ColumnAdapter<?> forGeometry(SQLColumn definition, GeometryIdentification ident) { + // In case of a computed column, geometric definition could be null. + final GeometryIdentification.GeometryColumn geomDef; + try { + geomDef = ident.fetch(definition).orElse(null); + } catch (SQLException | ParseException e) { + throw new BackingStoreException(e); + } + String geometryType = geomDef == null ? null : geomDef.type; + final Class geomClass = getGeometricClass(geometryType); + + if (geomDef == null || geomDef.crs == null) { + return new HexEWKBDynamicCrs(geomClass); + } 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); + } + } + + private Class getGeometricClass(String geometryType) { + if (geometryType == null) return library.rootClass; + + // remove Z, M or ZM suffix + if (geometryType.endsWith("M")) geometryType = geometryType.substring(0, geometryType.length()-1); + if (geometryType.endsWith("Z")) geometryType = geometryType.substring(0, geometryType.length()-1); + + final Class geomClass; + switch (geometryType) { + case "POINT": + geomClass = library.pointClass; + break; + case "LINESTRING": + geomClass = library.polylineClass; + break; + case "POLYGON": + geomClass = library.polygonClass; + break; + default: geomClass = library.rootClass; + } + return geomClass; + } + + @Override + public void close() throws SQLException { + identifyGeometries.close(); + } + + public static final class Spi implements DialectMapping.Spi { + + @Override + public Optional<DialectMapping> create(Connection c) throws SQLException { + try { + checkPostGISVersion(c); + } catch (SQLException e) { + final Logger logger = Logging.getLogger("org.apache.sis.internal.sql"); + logger.warning("No compatible PostGIS version found. Binding deactivated. See debug logs for more information"); + logger.log(Level.FINE, "Cannot determine PostGIS version", e); + return Optional.empty(); + } + return Optional.of(new PostGISMapping(this, c)); + } + + private void checkPostGISVersion(final Connection c) throws SQLException { + try ( + Statement st = c.createStatement(); + ResultSet result = st.executeQuery("SELECT PostGIS_version();"); + ) { + result.next(); + final String pgisVersion = result.getString(1); + if (!pgisVersion.startsWith("2.")) throw new SQLException("Incompatible PostGIS version. Only 2.x is supported for now, but database declares: "); + } + } + + @Override + public Dialect getDialect() { + return Dialect.POSTGRESQL; + } + } + + private abstract class Reader implements ColumnAdapter { + + final Class geomClass; + + public Reader(Class geomClass) { + this.geomClass = geomClass; + } + + @Override + public Class getJavaType() { + return geomClass; + } + } + + private final class WKBReader extends Reader implements SQLBiFunction<ResultSet, Integer, Object> { + + final CoordinateReferenceSystem crsToApply; + + private WKBReader(Class geomClass, CoordinateReferenceSystem crsToApply) { + super(geomClass); + this.crsToApply = crsToApply; + } + + @Override + public Object apply(ResultSet resultSet, Integer integer) throws SQLException { + final byte[] bytes = resultSet.getBytes(integer); + if (bytes == null) return null; + final Object value = library.parseWKB(bytes); + if (value != null && crsToApply != null) { + library.setCRS(value, crsToApply); + } + + return value; + } + + @Override + public SQLBiFunction prepare(Connection target) { + return this; + } + + @Override + public Optional<CoordinateReferenceSystem> getCrs() { + return Optional.ofNullable(crsToApply); + } + } + + private final class HexEWKBFixedCrs extends Reader { + final CoordinateReferenceSystem crsToApply; + + public HexEWKBFixedCrs(Class geomClass, CoordinateReferenceSystem crsToApply) { + super(geomClass); + this.crsToApply = crsToApply; + } + + @Override + public SQLBiFunction prepare(Connection target) { + return new HexEWKBReader(new EWKBReader(library).forCrs(crsToApply)); + } + + @Override + public Optional<CoordinateReferenceSystem> getCrs() { + return Optional.ofNullable(crsToApply); + } + } + + private final class HexEWKBDynamicCrs extends Reader { + + public HexEWKBDynamicCrs(Class geomClass) { + super(geomClass); + } + + @Override + public SQLBiFunction prepare(Connection target) { + // TODO: this component is not properly closed. As connection closing should also close this component + // statement, it should be Ok.However, a proper management would be better. + final CRSIdentification crsIdent; + try { + crsIdent = new CRSIdentification(target, sessionCache); + } catch (SQLException e) { + throw new BackingStoreException(e); + } + return new HexEWKBReader( + new EWKBReader(library) + .withResolver(crsIdent::fetchCrs) + ); + } + } + + private static final class HexEWKBReader implements SQLBiFunction<ResultSet, Integer, Object> { + + final EWKBReader reader; + + private HexEWKBReader(EWKBReader reader) { + this.reader = reader; + } + + @Override + public Object apply(ResultSet resultSet, Integer integer) throws SQLException { + final String hexa = resultSet.getString(integer); + return hexa == null ? null : reader.readHexa(hexa); + } + } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java index a117b13..af9bde8 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryFeatureSet.java @@ -123,7 +123,7 @@ public class QueryFeatureSet extends AbstractFeatureSet { * @throws SQLException If input query compiling or analysis of its metadata fails. */ public QueryFeatureSet(SQLBuilder queryBuilder, DataSource source, Connection conn) throws SQLException { - this(queryBuilder, new Analyzer(source, conn.getMetaData(), null, null), source, conn); + this(queryBuilder, new Analyzer(source, conn, null, null), source, conn); } @@ -195,34 +195,6 @@ public class QueryFeatureSet extends AbstractFeatureSet { return super.subset(query); } - /** - * Acquire a connection over parent database, forcing a few parameters to ensure optimal read performance and - * limiting user rights : - * <ul> - * <li>{@link Connection#setAutoCommit(boolean) auto-commit} to false</li> - * <li>{@link Connection#setReadOnly(boolean) querying read-only}</li> - * </ul> - * - * @param source Database pointer to create connection from. - * @return A new connection to database, with deactivated auto-commit. - * @throws SQLException If we cannot create a new connection. See {@link DataSource#getConnection()} for details. - */ - public static Connection connectReadOnly(final DataSource source) throws SQLException { - final Connection c = source.getConnection(); - try { - c.setAutoCommit(false); - c.setReadOnly(true); - } catch (SQLException e) { - try { - c.close(); - } catch (RuntimeException | SQLException bis) { - e.addSuppressed(bis); - } - throw e; - } - return c; - } - class SubsetAdapter extends SQLQueryAdapter { SubsetAdapter() { @@ -379,11 +351,11 @@ public class QueryFeatureSet extends AbstractFeatureSet { private abstract class QuerySpliterator implements java.util.Spliterator<Feature> { final ResultSet result; - final Connection origin; + final FeatureAdapter.ResultSetAdapter adapter; private QuerySpliterator(ResultSet result, Connection origin) { this.result = result; - this.origin = origin; + this.adapter = QueryFeatureSet.this.adapter.prepare(origin); } @Override @@ -408,7 +380,7 @@ public class QueryFeatureSet extends AbstractFeatureSet { public boolean tryAdvance(Consumer<? super Feature> action) { try { if (result.next()) { - final Feature f = adapter.read(result, origin); + final Feature f = adapter.read(result); action.accept(f); return true; } else return false; @@ -433,7 +405,7 @@ public class QueryFeatureSet extends AbstractFeatureSet { * there's not much improvement regarding to naive streaming approach. IMHO, two improvements would really impact * performance positively if done: * <ul> - * <li>Optimisation of batch loading through {@link FeatureAdapter#prefetch(int, ResultSet, Connection)}</li> + * <li>Optimisation of batch loading through {@link FeatureAdapter.ResultSetAdapter#prefetch(int, ResultSet)}</li> * <li>Better splitting balance, as stated by {@link Spliterator#trySplit()}</li> * </ul> */ @@ -493,7 +465,7 @@ public class QueryFeatureSet extends AbstractFeatureSet { if (chunk == null || idx >= chunk.size()) { idx = 0; try { - chunk = adapter.prefetch(fetchSize, result, origin); + chunk = adapter.prefetch(fetchSize, result); } catch (SQLException e) { throw new BackingStoreException(e); } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLCloseable.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLCloseable.java new file mode 100644 index 0000000..439b4f1 --- /dev/null +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLCloseable.java @@ -0,0 +1,8 @@ +package org.apache.sis.internal.sql.feature; + +import java.sql.SQLException; + +public interface SQLCloseable extends AutoCloseable { + @Override + void close() throws SQLException; +} diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLColumn.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLColumn.java index d92fe8c..68e6382 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLColumn.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SQLColumn.java @@ -1,58 +1,58 @@ package org.apache.sis.internal.sql.feature; +import java.sql.DatabaseMetaData; import java.sql.ResultSetMetaData; -import java.util.Optional; +import java.sql.Types; -import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.internal.metadata.sql.Reflection; +/** + * A simple POJO to hold information about an SQL column. This mainly represents information extracted from + * {@link DatabaseMetaData#getColumns(String, String, String, String) database metadata}. + * Note that for now, only a few selected information are represented. If needed, new fields could be added if needed. + * The aim is to describe as well as possible all SQL related information about a column, to allow mapping to feature + * model as accurate as possible. + */ class SQLColumn { + + /** + * Value type as specified in {@link Types} + */ final int type; + /** + * A name for the value type, free-text from the database engine. For more information about this, please see + * {@link DatabaseMetaData#getColumns(String, String, String, String)} and {@link Reflection#TYPE_NAME}. + */ final String typeName; - private final boolean isNullable; - private final ColumnRef naming; - private final int precision; - - SQLColumn(int type, String typeName, boolean isNullable, ColumnRef naming, int precision) { - this.type = type; - this.typeName = typeName; - this.isNullable = isNullable; - this.naming = naming; - this.precision = precision; - } - - public ColumnRef getName() { - return naming; - } - - public int getType() { - return type; - } - - public String getTypeName() { - return typeName; - } + final boolean isNullable; - public boolean isNullable() { - return isNullable; - } + /** + * Name of the column, optionally with an alias, in case of a query analysis. + */ + final ColumnRef naming; /** - * Same as {@link ResultSetMetaData#getPrecision(int)}. - * @return 0 if unknown. For texts, maximum number of characters allowed. For numerics, max precision. For blobs, - * number of bytes allowed. + * Same as {@link ResultSetMetaData#getPrecision(int)}. It will be 0 if unknown. For texts, it represents maximum + * number of characters allowed. For numbers, its maximum precision. For blobs, a limit in allowed number of bytes. */ - public int getPrecision() { - return precision; - } + final int precision; /** - * TODO: implement. - * Note : This method could be used not only for geometric fields, but also on numeric ones representing 1D - * systems. - * - * @return null for now, implementation needed. + * Optional. The table that contains this column. It could be null in case this column specification is done from + * query analysis. */ - public Optional<CoordinateReferenceSystem> getCrs() { - return Optional.empty(); + final TableReference origin; + + SQLColumn(int type, String typeName, boolean isNullable, ColumnRef naming, int precision) { + this(type, typeName, isNullable, naming, precision, null); + } + + SQLColumn(int type, String typeName, boolean isNullable, ColumnRef naming, int precision, TableReference origin) { + this.type = type; + this.typeName = typeName; + this.isNullable = isNullable; + this.naming = naming; + this.precision = precision; + this.origin = origin; } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java index ca5ff05..01b59b8 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SpatialFunctions.java @@ -16,6 +16,7 @@ */ package org.apache.sis.internal.sql.feature; +import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; @@ -60,7 +61,7 @@ class SpatialFunctions { /** * Creates a new accessor to geospatial functions for the database described by given metadata. */ - SpatialFunctions(final DatabaseMetaData metadata) throws SQLException { + SpatialFunctions(final Connection c, final DatabaseMetaData metadata) throws SQLException { /* * Get information about whether byte are unsigned. * According JDBC specification, the rows shall be ordered by DATA_TYPE. @@ -83,7 +84,7 @@ class SpatialFunctions { library = null; final Dialect dialect = Dialect.guess(metadata); - specificMapping = forDialect(dialect); + specificMapping = forDialect(dialect, c); defaultMapping = new ANSIMapping(isByteUnsigned); } @@ -96,14 +97,13 @@ class SpatialFunctions { * <p>The default implementation handles the types declared in {@link Types} class. * Subclasses should handle the geometry types declared by spatial extensions.</p> * - * @param sqlType SQL type code as one of {@link java.sql.Types} constants. - * @param sqlTypeName data source dependent type name. For User Defined Type (UDT) the name is fully qualified. + * @param columnDefinition Definition of source database column, including its SQL type and type name. * @return corresponding java type, or {@code null} if unknown. */ @SuppressWarnings("fallthrough") - protected ColumnAdapter<?> toJavaType(final int sqlType, final String sqlTypeName) { - return specificMapping.flatMap(dialect -> dialect.getMapping(sqlType, sqlTypeName)) - .orElseGet(() -> defaultMapping.getMappingImpl(sqlType, sqlTypeName)); + protected ColumnAdapter<?> toJavaType(final SQLColumn columnDefinition) { + return specificMapping.flatMap(dialect -> dialect.getMapping(columnDefinition)) + .orElseGet(() -> defaultMapping.getMappingImpl(columnDefinition)); } /** @@ -121,9 +121,9 @@ class SpatialFunctions { return null; } - static Optional<DialectMapping> forDialect(final Dialect dialect) { + static Optional<DialectMapping> forDialect(final Dialect dialect, Connection c) throws SQLException { switch (dialect) { - case POSTGRESQL: return Optional.of(new PostGISMapping()); + case POSTGRESQL: return new PostGISMapping.Spi().create(c); default: return Optional.empty(); } } diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/StreamSQL.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/StreamSQL.java index 7291a04..ff72c02 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/StreamSQL.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/StreamSQL.java @@ -48,6 +48,7 @@ import org.apache.sis.storage.DataStoreException; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.logging.Logging; +import static org.apache.sis.internal.sql.feature.Database.connectReadOnly; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; import static org.apache.sis.util.ArgumentChecks.ensurePositive; @@ -179,7 +180,7 @@ class StreamSQL extends StreamDecoration<Feature> { // If underlying connector does not support query estimation, we will fallback on brut-force counting. return super.count(); } - try (Connection conn = QueryFeatureSet.connectReadOnly(source)) { + try (Connection conn = connectReadOnly(source)) { try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) { if (rs.next()) { @@ -194,7 +195,7 @@ class StreamSQL extends StreamDecoration<Feature> { @Override protected synchronized Stream<Feature> createDecoratedStream() { final AtomicReference<Connection> connectionRef = new AtomicReference<>(); - Stream<Feature> featureStream = Stream.of(uncheck(() -> QueryFeatureSet.connectReadOnly(source))) + Stream<Feature> featureStream = Stream.of(uncheck(() -> connectReadOnly(source))) .map(Supplier::get) .peek(connectionRef::set) .flatMap(conn -> { 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 7205f70..22c9aba 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 @@ -200,7 +200,7 @@ final class Table extends AbstractFeatureSet { this.hasGeometry = specification.getPrimaryGeometryColumn().isPresent(); this.attributes = Collections.unmodifiableList( specification.getColumns().stream() - .map(SQLColumn::getName) + .map(column -> column.naming) .collect(Collectors.toList()) ); }
