sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ama...@apache.org
Subject [sis] 43/45: feat(SQLStore): Improve PostGIS geometry binding
Date Tue, 12 Nov 2019 16:45:10 GMT
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 325ce8ec615f8d91902fbdf2a411f1ba111d208a
Author: Alexis Manin <amanin@apache.org>
AuthorDate: Tue Nov 12 16:15:08 2019 +0100

    feat(SQLStore): Improve PostGIS geometry binding
    
    Add dynamic CRS support
    
    Add multi-geometry support
---
 .../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/ANSIMapping.java      |  18 +--
 .../apache/sis/internal/sql/feature/Analyzer.java  |   6 +-
 .../internal/sql/feature/CRSIdentification.java    |  91 +++++++++++++
 .../sis/internal/sql/feature/ColumnAdapter.java    |  69 +++++-----
 .../sis/internal/sql/feature/EWKBReader.java       | 114 +++++++++++++----
 .../sis/internal/sql/feature/FeatureAdapter.java   |  82 ++++++++----
 .../apache/sis/internal/sql/feature/Features.java  |   6 +-
 .../sql/feature/GeometryIdentification.java        |  71 ++---------
 .../sis/internal/sql/feature/PostGISMapping.java   | 141 +++++++++++++++++----
 .../sis/internal/sql/feature/QueryFeatureSet.java  |  10 +-
 15 files changed, 478 insertions(+), 222 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/ANSIMapping.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIMapping.java
index ef6403b..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
@@ -55,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;
         }
     }
@@ -96,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 9878a5e..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
@@ -353,13 +353,9 @@ final class Analyzer {
         for (SQLColumn col : spec.getColumns()) {
             i++;
             final ColumnAdapter<?> colAdapter = functions.toJavaType(col);
-            Class<?> type = colAdapter.javaType;
+            Class<?> type = colAdapter.getJavaType();
             final String colName = col.naming.getColumnName();
             final String attrName = col.naming.getAttributeName();
-            if (type == null) {
-                warning(Resources.Keys.UnknownType_1, colName);
-                type = Object.class;
-            }
 
             final AttributeTypeBuilder<?> attribute = builder
                     .addAttribute(type)
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 da13ae4..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,9 +1,8 @@
 package org.apache.sis.internal.sql.feature;
 
+import java.sql.Connection;
 import java.sql.ResultSet;
-import java.sql.SQLException;
 import java.util.Optional;
-import java.util.function.Function;
 
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 
@@ -15,39 +14,51 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
  *
  * @param <T> Type of object decoded from cell.
  */
-public class ColumnAdapter<T> implements SQLBiFunction<ResultSet, Integer, T> {
-    final Class<T> javaType;
-    private final SQLBiFunction<ResultSet, Integer, T> fetchValue;
-    private final CoordinateReferenceSystem crs;
+public interface ColumnAdapter<T> {
 
-    public ColumnAdapter(Class<T> javaType, SQLBiFunction<ResultSet, Integer, T> fetchValue) {
-        this(javaType, fetchValue, null);
-    }
-
-    public ColumnAdapter(Class<T> javaType, SQLBiFunction<ResultSet, Integer, T> fetchValue, final CoordinateReferenceSystem crs) {
-        ensureNonNull("Result java type", javaType);
-        ensureNonNull("Function for value retrieval", fetchValue);
-        this.javaType = javaType;
-        this.fetchValue = fetchValue;
-        this.crs = crs;
-    }
-
-    @Override
-    public T apply(ResultSet resultSet, Integer integer) throws SQLException {
-        return fetchValue.apply(resultSet, integer);
-    }
-
-    @Override
-    public <V> SQLBiFunction<ResultSet, Integer, V> andThen(Function<? super T, ? extends V> after) {
-        return fetchValue.andThen(after);
-    }
+    /**
+     * 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.
      */
-    public Optional<CoordinateReferenceSystem> getCrs() {
-        return Optional.ofNullable(crs);
+    default Optional<CoordinateReferenceSystem> getCrs() {
+        return Optional.empty();
+    }
+
+    /**
+     *
+     * @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/EWKBReader.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/EWKBReader.java
index 926dbc5..8f4bc1f 100644
--- 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
@@ -3,6 +3,10 @@ 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;
 
@@ -20,6 +24,10 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty;
  * 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)
  */
@@ -39,9 +47,9 @@ class EWKBReader {
     private static final int MULTIPOLYGON = 6;
     private static final int GEOMETRYCOLLECTION = 7;
 
-    private final Geometries factory;
+    final Geometries factory;
 
-    private CoordinateReferenceSystem crs;
+    private final Function<ByteBuffer, ?> decoder;
 
     EWKBReader() {
         this((GeometryLibrary) null);
@@ -52,12 +60,38 @@ class EWKBReader {
     }
 
     EWKBReader(Geometries geometryFactory) {
-        this.factory = geometryFactory;
+        this(geometryFactory, bytes -> new Reader(geometryFactory, bytes).read());
+    }
+
+    private EWKBReader(final Geometries factory, Function<ByteBuffer, ?> decoder) {
+        this.factory = factory;
+        this.decoder = decoder;
     }
 
-    public EWKBReader setCrs(CoordinateReferenceSystem defaultCrs) {
-        this.crs = defaultCrs;
-        return this;
+    /**
+     *
+     * @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) {
@@ -65,16 +99,22 @@ class EWKBReader {
     }
 
     Object read(final byte[] eWkb) {
-        return new Reader(ByteBuffer.wrap(eWkb)).read();
+        return decoder.apply(ByteBuffer.wrap(eWkb));
+    }
+
+    Object read(final ByteBuffer eWkb) {
+        return decoder.apply(eWkb);
     }
 
-    private final class Reader {
+    private static final class Reader {
+        final Geometries factory;
         final ByteBuffer buffer;
         final int geomType;
         final int dimension;
         final int srid;
 
-        private Reader(ByteBuffer buffer) {
+        private Reader(Geometries factory, ByteBuffer buffer) {
+            this.factory = factory;
             final byte endianess = buffer.get();
             if (isLittleEndian(endianess)) {
                 this.buffer = buffer.order(ByteOrder.LITTLE_ENDIAN);
@@ -89,17 +129,6 @@ class EWKBReader {
         }
 
         Object read() {
-            final Object geom = decodeGeometry();
-
-            // TODO: set CRS
-            if (crs != null) {
-            } else if (srid > 0) {
-            }
-
-            return geom;
-        }
-
-        Object decodeGeometry() {
             switch (geomType) {
                 case POINT:              return readPoint();
                 case LINESTRING:         return readLineString();
@@ -114,11 +143,28 @@ class EWKBReader {
         }
 
         private Object readMultiLineString() {
-            throw new UnsupportedOperationException();
+            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() {
-            throw new UnsupportedOperationException();
+            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() {
@@ -143,7 +189,7 @@ class EWKBReader {
             final double[] nans = new double[dimension];
             Arrays.fill(nans, Double.NaN);
             final Vector separator = Vector.create(nans);
-            final Vector[] allShells = new Vector[nbRings + nbRings -1]; // include ring separators
+            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;
@@ -158,15 +204,33 @@ class EWKBReader {
         }
 
         private double[] readCoordinateSequence() {
-            return readCoordinateSequence(buffer.getInt());
+            return readCoordinateSequence(readCount());
         }
 
         private double[] readCoordinateSequence(int nbPts) {
-            final double[] brutPoint = new double[dimension*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) {
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
index a10932c..bb5c67e 100644
--- 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
@@ -12,11 +12,8 @@ import java.util.Optional;
 import java.util.Set;
 
 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.Cache;
 
 import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty;
 
@@ -27,7 +24,6 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty;
  */
 class GeometryIdentification implements SQLCloseable {
 
-    private final Connection c;
     final PreparedStatement identifySchemaQuery;
     /**
      * A statement serving two purposes:
@@ -39,9 +35,7 @@ class GeometryIdentification implements SQLCloseable {
      * @implNote The statement definition is able to serve both purposes by changing geometry column filter to no-op.
      */
     final PreparedStatement columnQuery;
-    final PreparedStatement wktFromSrid;
-
-    private final WKTFormat wktReader;
+    final CRSIdentification crsIdent;
 
     /**
      * Describes if geometry column registry include a column for geometry types, according that one can apparently omit
@@ -50,11 +44,11 @@ class GeometryIdentification implements SQLCloseable {
      */
     final boolean typeIncluded;
 
-    public GeometryIdentification(Connection c) throws SQLException {
-        this(c, "geometry_columns", "f_geometry_column", "geometry_type");
+    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) throws SQLException {
-        this.c = c;
+
+    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(
@@ -64,9 +58,7 @@ class GeometryIdentification implements SQLCloseable {
                 "AND f_table_name = ? " +
                 "AND "+geometryColumnName+" LIKE ?"
         );
-        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);
+        crsIdent = new CRSIdentification(c, sessionCache);
     }
 
     Set<GeometryColumn> fetchGeometricColumns(String schema, final String table) throws SQLException, ParseException {
@@ -120,61 +112,16 @@ class GeometryIdentification implements SQLCloseable {
         // 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 = fetchCrs(pgSrid);
+        final CoordinateReferenceSystem crs = crsIdent.fetchCrs(pgSrid);
         return new GeometryColumn(name, dimension, pgSrid, type, crs);
     }
 
-    /**
-     * 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.
-     */
-    private CoordinateReferenceSystem fetchCrs(int pgSrid) throws SQLException, IllegalArgumentException, ParseException {
-        if (pgSrid <= 0) return null;
-
-        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();
-        }
-    }
-
     @Override
     public void close() throws SQLException {
         try (
                 SQLCloseable c1 = columnQuery::close;
                 SQLCloseable c2 = identifySchemaQuery::close;
-                SQLCloseable c3 = wktFromSrid::close;
+                SQLCloseable c3 = crsIdent
          ) {}
     }
 
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 84144c8..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
@@ -3,15 +3,20 @@ 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 {
 
@@ -21,13 +26,23 @@ public final class PostGISMapping implements DialectMapping {
 
     final Connection connection;
 
-    final Geometries<?> library;
+    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;
-        this.identifyGeometries = new GeometryIdentification(c, "geometry_columns", "f_geometry_column", "type");
-        this.identifyGeographies = new GeometryIdentification(c, "geography_columns", "f_geography_column", "type");
+        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);
     }
@@ -65,16 +80,14 @@ public final class PostGISMapping implements DialectMapping {
         }
         String geometryType = geomDef == null ? null : geomDef.type;
         final Class geomClass = getGeometricClass(geometryType);
-        SQLBiFunction<ResultSet, Integer, Object> geometryDecoder;
-        // TODO: activate optimisation : WKB is lighter, but we need to modify user query, and to know CRS in advance.
-//        if (geomDef.crs == null) {
-            geometryDecoder = new HexEWKBReader();
-//        } else {
-//            // For optimisation purpose, we'll return directly a WKB reader if CRS is known in advance.
-//            geometryDecoder = new WKBReader(geomDef.crs);
-//        }
-
-        return new ColumnAdapter<>(geomClass, geometryDecoder, geomDef == null ? null : geomDef.crs);
+
+        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) {
@@ -109,20 +122,54 @@ public final class PostGISMapping implements DialectMapping {
 
         @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 final class WKBReader implements SQLBiFunction<ResultSet, Integer, Object> {
+    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(CoordinateReferenceSystem crsToApply) {
+        private WKBReader(Class geomClass, CoordinateReferenceSystem crsToApply) {
+            super(geomClass);
             this.crsToApply = crsToApply;
         }
 
@@ -131,24 +178,72 @@ public final class PostGISMapping implements DialectMapping {
             final byte[] bytes = resultSet.getBytes(integer);
             if (bytes == null) return null;
             final Object value = library.parseWKB(bytes);
-            if (value != null) {
-                // TODO: set CRS
+            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 HexEWKBReader implements SQLBiFunction<ResultSet, Integer, Object> {
+    private final class HexEWKBFixedCrs extends Reader {
+        final CoordinateReferenceSystem crsToApply;
 
-        final EWKBReader reader;
+        public HexEWKBFixedCrs(Class geomClass, CoordinateReferenceSystem crsToApply) {
+            super(geomClass);
+            this.crsToApply = crsToApply;
+        }
 
-        private HexEWKBReader() {
-            this(null);
+        @Override
+        public SQLBiFunction prepare(Connection target) {
+            return new HexEWKBReader(new EWKBReader(library).forCrs(crsToApply));
         }
 
-        private HexEWKBReader(CoordinateReferenceSystem crsToApply) {
-            reader = new EWKBReader(library).setCrs(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
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 7b94f2f..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
@@ -351,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
@@ -380,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;
@@ -405,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>
      */
@@ -465,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);
                 }


Mime
View raw message