sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ama...@apache.org
Subject [sis] 16/22: feat(SQLStore): Add partial PostGIS support : geometries and geographies.
Date Thu, 14 Nov 2019 11:46:50 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 237a71aba54f100c60bd4f2243f4967023b0bf09
Author: Alexis Manin <amanin@apache.org>
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())
         );
     }


Mime
View raw message