This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit a6b1bfaeca683f0da95b83e88f2452a5efe9381a
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Aug 16 13:32:21 2024 +0200

    Add `findCRS(int)` and `findSRID(CoordinateReferenceSystem)` public methods 
in `SQLStore`.
    This is in replacemen for equivalent methods in the geopackage module.
---
 .../main/org/apache/sis/storage/sql/SQLStore.java  | 230 ++++++++++++++++---
 .../apache/sis/storage/sql/feature/Database.java   | 145 ++++++------
 .../sis/storage/sql/feature/InfoStatements.java    | 244 +++++++++++++++------
 .../sis/storage/sql/feature/SchemaModifier.java    |   2 +-
 .../sis/storage/sql/feature/ValueGetter.java       |   6 +-
 .../sis/storage/sql/postgis/ExtentEstimator.java   |   2 +-
 .../apache/sis/storage/sql/postgis/Postgres.java   |  11 +-
 .../storage/sql/feature/InfoStatementsTest.java    |   2 +-
 8 files changed, 475 insertions(+), 167 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java
index 2dec07e23d..44b7d958c6 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java
@@ -20,17 +20,23 @@ import java.util.Map;
 import java.util.LinkedHashMap;
 import java.util.Optional;
 import java.util.Collection;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 import javax.sql.DataSource;
 import java.sql.Connection;
 import java.lang.reflect.Method;
 import org.opengis.util.GenericName;
 import org.opengis.metadata.Metadata;
 import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.metadata.spatial.SpatialRepresentationType;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.NoSuchDataException;
 import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.event.StoreEvent;
@@ -38,6 +44,7 @@ import org.apache.sis.storage.event.StoreListener;
 import org.apache.sis.storage.event.WarningEvent;
 import org.apache.sis.storage.sql.feature.Database;
 import org.apache.sis.storage.sql.feature.Resources;
+import org.apache.sis.storage.sql.feature.InfoStatements;
 import org.apache.sis.storage.sql.feature.SchemaModifier;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.util.ArgumentChecks;
@@ -56,7 +63,7 @@ import org.apache.sis.setup.OptionKey;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.5
  * @since   1.0
  */
 public class SQLStore extends DataStore implements Aggregate {
@@ -73,6 +80,8 @@ public class SQLStore extends DataStore implements Aggregate {
 
     /**
      * The data source to use for obtaining connections to the database.
+     *
+     * @see #getDataSource()
      */
     private final DataSource source;
 
@@ -84,8 +93,11 @@ public class SQLStore extends DataStore implements Aggregate 
{
     /**
      * The result of inspecting database schema for deriving {@link 
org.opengis.feature.FeatureType}s.
      * Created when first needed. May be discarded and recreated if the store 
needs a refresh.
+     *
+     * @see #model()
+     * @see #model(Connection)
      */
-    private Database<?> model;
+    private volatile Database<?> model;
 
     /**
      * Fully qualified names (including catalog and schema) of the tables to 
include in this store.
@@ -120,6 +132,23 @@ public class SQLStore extends DataStore implements 
Aggregate {
      */
     private final SchemaModifier customizer;
 
+    /**
+     * The lock for read or write operations in the SQL database, or {@code 
null} if none.
+     * The read or write lock should be obtained before to get a connection 
for executing
+     * a statement, and released after closing the connection. Locking is 
assumed unneeded
+     * for obtaining database metadata.
+     *
+     * <p>This field should be null if the database manages concurrent 
transactions by itself.
+     * It is non-null only as a workaround for databases that do not support 
concurrency.</p>
+     *
+     * <p>This lock is not used for cache integrity. The {@code SQLStore} 
caches are protected
+     * by classical synchronized statements.</p>
+     *
+     * @see #lock(boolean)
+     * @see Database#transactionLocks
+     */
+    private final ReadWriteLock transactionLocks;
+
     /**
      * Creates a new {@code SQLStore} for the given data source and tables, 
views or queries.
      * The given {@code connector} shall contain a {@link DataSource} instance.
@@ -166,9 +195,18 @@ public class SQLStore extends DataStore implements 
Aggregate {
         }
         this.tableNames = ArraysExt.resize(tableNames, tableCount);
         this.queries    = ArraysExt.resize(queries,    queryCount);
-        if (getClass() == SQLStore.class) {
-            listeners.useReadOnlyEvents();
-        }
+        transactionLocks = new ReentrantReadWriteLock();    // TODO: make 
optional.
+    }
+
+    /**
+     * The data source to use for obtaining connections to the database.
+     * This is the data source specified at construction time.
+     *
+     * @return the data source to use for obtaining connections to the 
database.
+     * @since 1.5
+     */
+    public final DataSource getDataSource() {
+        return source;
     }
 
     /**
@@ -210,18 +248,22 @@ public class SQLStore extends DataStore implements 
Aggregate {
 
     /**
      * Returns the database model, analyzing the database schema when first 
needed.
+     * This method is thread-safe.
      */
-    private synchronized Database<?> model() throws DataStoreException {
-        if (model == null) {
-            try (Connection c = source.getConnection()) {
-                model = Database.create(this, source, c, geomLibrary, 
tableNames, queries, customizer, listeners);
-            } catch (DataStoreException e) {
-                throw e;
-            } catch (Exception e) {
-                throw new DataStoreException(Exceptions.unwrap(e));
+    private Database<?> model() throws DataStoreException {
+        Database<?> current = model;
+        if (current == null) {
+            synchronized (this) {
+                try (Connection c = source.getConnection()) {
+                    current = model(c);
+                } catch (DataStoreException e) {
+                    throw e;
+                } catch (Exception e) {
+                    throw new DataStoreException(Exceptions.unwrap(e));
+                }
             }
         }
-        return model;
+        return current;
     }
 
     /**
@@ -232,10 +274,13 @@ public class SQLStore extends DataStore implements 
Aggregate {
      * @param c  connection to the database.
      */
     private Database<?> model(final Connection c) throws Exception {
-        if (model == null) {
-            model = Database.create(this, source, c, geomLibrary, tableNames, 
queries, customizer, listeners);
+        assert Thread.holdsLock(this);
+        Database<?> current = model;
+        if (current == null) {
+            current = Database.create(this, source, c, geomLibrary, 
tableNames, queries, customizer, listeners, transactionLocks);
+            model = current;
         }
-        return model;
+        return current;
     }
 
     /**
@@ -248,18 +293,12 @@ public class SQLStore extends DataStore implements 
Aggregate {
     @Override
     public synchronized Metadata getMetadata() throws DataStoreException {
         if (metadata == null) {
-            final MetadataBuilder builder = new MetadataBuilder();
+            final var builder = new MetadataBuilder();
             
builder.addSpatialRepresentation(SpatialRepresentationType.TEXT_TABLE);
             try (Connection c = source.getConnection()) {
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Database<?> model = model(c);
-                if (model.hasGeometry()) {
-                    
builder.addSpatialRepresentation(SpatialRepresentationType.VECTOR);
-                }
-                if (model.hasRaster()) {
-                    
builder.addSpatialRepresentation(SpatialRepresentationType.GRID);
-                }
-                model.listTables(c.getMetaData(), builder);
+                model.metadata(c.getMetaData(), builder);
             } catch (DataStoreException e) {
                 throw e;
             } catch (Exception e) {
@@ -317,6 +356,132 @@ public class SQLStore extends DataStore implements 
Aggregate {
         return model().findTable(this, identifier);
     }
 
+    /**
+     * Returns the coordinate reference system associated to the given 
identifier. The spatial reference
+     * system identifiers (<abbr>SRID</abbr>) are the primary keys of the 
{@code "SPATIAL_REF_SYS"} table
+     * (the name of that table may vary depending on which spatial schema 
standard is used).
+     * Those identifiers are specific to each database and are not necessarily 
related to EPSG codes.
+     * They should be considered as opaque identifiers.
+     *
+     * <h4>Undefined <abbr>CRS</abbr></h4>
+     * Some standards such as Geopackage define 0 as "undefined geographic 
<abbr>CRS</abbr>" and -1 as
+     * "undefined Cartesian <abbr>CRS</abbr>". This method returns {@code 
null} for all undefined <abbr>CRS</abbr>,
+     * regardless their type. No default value is returned because this class 
cannot guess the datum and units of
+     * measurement of an undefined <abbr>CRS</abbr>. All <abbr>SRID</abbr> 
equal or less than zero are considered
+     * undefined.
+     *
+     * <h4>Axis order</h4>
+     * Some standards such as Geopackage mandate (east, north) axis order. 
{@code SQLStore} uses the axis order
+     * as defined in the <abbr>WKT</abbr> descriptions of the {@code 
"SPATIAL_REF_SYS"} table. No reordering is
+     * applied. It is data producer's responsibility to provide definitions 
with the expected axis order.
+     *
+     * @param  srid  a primary key value of the {@code "SPATIAL_REF_SYS"} 
table.
+     * @return the <abbr>CRS</abbr> associated to the given <abbr>SRID</abbr>, 
or {@code null} if the given
+     *         <abbr>SRID</abbr> is a code explicitly associated to an 
undefined <abbr>CRS</abbr>.
+     * @throws NoSuchDataException if no <abbr>CRS</abbr> is associated to the 
given <abbr>SRID</abbr>.
+     * @throws DataStoreException if the query failed for another reason.
+     *
+     * @since 1.5
+     */
+    public CoordinateReferenceSystem findCRS(final int srid) throws 
DataStoreException {
+        if (srid <= 0) {
+            return null;
+        }
+        Database<?> database = model;
+        CoordinateReferenceSystem crs;
+        if (database == null || (crs = database.getCachedCRS(srid)) == null) {
+            final Lock lock = lock(false);
+            try {
+                try (Connection c = source.getConnection()) {
+                    if (database == null) {
+                        synchronized (this) {
+                            database = model(c);
+                        }
+                    }
+                    try (InfoStatements info = 
database.createInfoStatements(c)) {
+                        crs = info.fetchCRS(srid);
+                    }
+                } catch (DataStoreContentException e) {
+                    throw new NoSuchDataException(e.getMessage(), 
e.getCause());
+                } catch (DataStoreException e) {
+                    throw e;
+                } catch (Exception e) {
+                    throw new DataStoreException(Exceptions.unwrap(e));
+                }
+            } finally {
+                if (lock != null) {
+                    lock.unlock();
+                }
+            }
+        }
+        return crs;
+    }
+
+    /**
+     * Returns the <abbr>SRID</abbr> associated to the given spatial reference 
system.
+     * This method is the converse of {@link #findCRS(int)}.
+     *
+     * @param  crs       the CRS for which to find a SRID, or {@code null}.
+     * @param  allowAdd  whether to allow the addition of a new row in the 
{@code "SPATIAL_REF_SYS"} table
+     *                   if the given <abbr>CRS</abbr> is not found.
+     * @return SRID for the given <abbr>CRS</abbr>, or 0 if the given 
<abbr>CRS</abbr> was null.
+     * @throws DataStoreException if an <abbr>SQL</abbr> error, parsing error 
or other error occurred.
+     *
+     * @since 1.5
+     */
+    public int findSRID(final CoordinateReferenceSystem crs, final boolean 
allowAdd) throws DataStoreException {
+        if (crs == null) {
+            return 0;
+        }
+        Database<?> database = model;
+        if (database != null) {
+            Integer srid = database.getCachedSRID(crs);
+            if (srid != null) return srid;
+        }
+        final int srid;
+        final Lock lock = lock(allowAdd);
+        try {
+            try (Connection c = source.getConnection()) {
+                if (database == null) {
+                    synchronized (this) {
+                        database = model(c);
+                    }
+                }
+                if (!allowAdd && database.dialect.supportsReadOnlyUpdate()) {
+                    c.setReadOnly(true);
+                }
+                try (InfoStatements info = database.createInfoStatements(c)) {
+                    srid = info.findSRID(crs);
+                }
+            } catch (DataStoreException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new DataStoreException(Exceptions.unwrap(e));
+            }
+        } finally {
+            if (lock != null) {
+                lock.unlock();
+            }
+        }
+        return srid;
+    }
+
+    /**
+     * Returns a read or write lock, or {@code null} if none.
+     * The call to {@link Lock#lock()} will be already performed.
+     *
+     * @param  write  {@code true} for the write lock, or {@code false} for 
the read lock.
+     * @return the requested lock, or {@code null} if none.
+     */
+    private Lock lock(final boolean write) {
+        if (transactionLocks == null) {
+            return null;
+        }
+        final Lock lock = write ? transactionLocks.writeLock() : 
transactionLocks.readLock();
+        lock.lock();
+        return lock;
+    }
+
     /**
      * Registers a listener to notify when the specified kind of event occurs 
in this data store.
      * The current implementation of this data store can emit only {@link 
WarningEvent}s;
@@ -330,6 +495,18 @@ public class SQLStore extends DataStore implements 
Aggregate {
         }
     }
 
+    /**
+     * Clears the cache so that next operations will reload all needed 
information from the database.
+     * This method can be invoked when the database content has been modified 
by a process other than
+     * the methods in this class.
+     *
+     * @since 1.5
+     */
+    public synchronized void refresh() {
+        model    = null;
+        metadata = null;
+    }
+
     /**
      * Closes this SQL store and releases any underlying resources.
      *
@@ -339,6 +516,7 @@ public class SQLStore extends DataStore implements 
Aggregate {
     public synchronized void close() throws DataStoreException {
         listeners.close();      // Should never fail.
         // There is no JDBC connection to close here.
-        model = null;
+        model    = null;
+        metadata = null;
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java
index 66010d1a6b..23cf211df8 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java
@@ -29,7 +29,6 @@ import java.util.Locale;
 import java.util.Optional;
 import java.util.logging.LogRecord;
 import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.sql.Array;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
@@ -41,6 +40,7 @@ import javax.sql.DataSource;
 import org.opengis.util.GenericName;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.metadata.spatial.SpatialRepresentationType;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.metadata.sql.privy.Syntax;
 import org.apache.sis.metadata.sql.privy.Dialect;
@@ -161,16 +161,12 @@ public class Database<G> extends Syntax  {
     /**
      * {@code true} if this database contains at least one geometry column.
      * This field is initialized by {@link #analyze analyze(…)} and shall not 
be modified after that point.
-     *
-     * @see #hasGeometry()
      */
     private boolean hasGeometry;
 
     /**
      * {@code true} if this database contains at least one raster column.
      * This field is initialized by {@link #analyze analyze(…)} and shall not 
be modified after that point.
-     *
-     * @see #hasRaster()
      */
     private boolean hasRaster;
 
@@ -246,10 +242,11 @@ public class Database<G> extends Syntax  {
      * @param  dialect      additional information not provided by {@code 
metadata}.
      * @param  geomLibrary  the factory to use for creating geometric objects.
      * @param  listeners    where to send warnings.
+     * @param  locks        the read/write locks, or {@code null} if none.
      * @throws SQLException if an error occurred while reading database 
metadata.
      */
     protected Database(final DataSource source, final DatabaseMetaData 
metadata, final Dialect dialect,
-                       final Geometries<G> geomLibrary, final StoreListeners 
listeners)
+                       final Geometries<G> geomLibrary, final StoreListeners 
listeners, final ReadWriteLock locks)
             throws SQLException
     {
         super(metadata, true);
@@ -292,7 +289,7 @@ public class Database<G> extends Syntax  {
         supportsSchemas    = metadata.supportsSchemasInDataManipulation();
         supportsJavaTime   = dialect.supportsJavaTime();
         crsEncodings       = EnumSet.noneOf(CRSEncoding.class);
-        transactionLocks   = dialect.supportsConcurrency() ? null : new 
ReentrantReadWriteLock();
+        transactionLocks   = dialect.supportsConcurrency() ? null : locks;
     }
 
     /**
@@ -306,13 +303,14 @@ public class Database<G> extends Syntax  {
      * @param  queries      additional resources associated to SQL queries. 
Specified by users at construction time.
      * @param  customizer   user-specified modification to the features, or 
{@code null} if none.
      * @param  listeners    where to send warnings.
+     * @param  locks        the read/write locks, or {@code null} if none.
      * @return handler for the spatial database.
      * @throws SQLException if a database error occurred while reading 
metadata.
      * @throws DataStoreException if a logical error occurred while analyzing 
the database structure.
      */
     public static Database<?> create(final SQLStore store, final DataSource 
source, final Connection connection,
             final GeometryLibrary geomLibrary, final GenericName[] tableNames, 
final ResourceDefinition[] queries,
-            final SchemaModifier customizer, final StoreListeners listeners)
+            final SchemaModifier customizer, final StoreListeners listeners, 
final ReadWriteLock locks)
             throws Exception
     {
         final DatabaseMetaData metadata = connection.getMetaData();
@@ -320,11 +318,8 @@ public class Database<G> extends Syntax  {
         final Dialect dialect = Dialect.guess(metadata);
         final Database<?> db;
         switch (dialect) {
-            case POSTGRESQL: db = new Postgres<>(source, connection, metadata, 
dialect, g, listeners); break;
-            default: {
-                db = new Database<>(source, metadata, dialect, g, listeners);
-                break;
-            }
+            case POSTGRESQL: db = new Postgres<>(source, connection, metadata, 
dialect, g, listeners, locks); break;
+            default:         db = new Database<>(source,             metadata, 
dialect, g, listeners, locks); break;
         }
         db.analyze(store, connection, tableNames, queries, customizer);
         return db;
@@ -521,7 +516,13 @@ public class Database<G> extends Syntax  {
      * @param  builder   where to add information about the tables.
      * @throws SQLException if an error occurred while fetching table 
information.
      */
-    public final void listTables(final DatabaseMetaData metadata, final 
MetadataBuilder builder) throws SQLException {
+    public final void metadata(final DatabaseMetaData metadata, final 
MetadataBuilder builder) throws SQLException {
+        if (hasGeometry) {
+            builder.addSpatialRepresentation(SpatialRepresentationType.VECTOR);
+        }
+        if (hasRaster) {
+            builder.addSpatialRepresentation(SpatialRepresentationType.GRID);
+        }
         for (final Table table : tables) {
             builder.addFeatureType(table.featureType, 
table.countRows(metadata, false, false));
         }
@@ -551,14 +552,14 @@ public class Database<G> extends Syntax  {
     }
 
     /**
-     * Appends a call to a function or table defined in the spatial schema.
+     * Appends a table or a call to a function defined in the spatial schema.
      * The name will be prefixed by catalog and schema name if applicable.
      * The name will not be quoted.
      *
-     * @param  sql   the SQL builder where to add the spatial function or 
table name.
-     * @param  name  the function or table to append.
+     * @param  sql   the SQL builder where to add the spatial table or 
function.
+     * @param  name  name of the table or function to append.
      */
-    public final void appendSpatialSchema(final SQLBuilder sql, final String 
name) {
+    public final void formatTableName(final SQLBuilder sql, final String name) 
{
         final String schema = schemaOfSpatialTables;
         if (schema != null && !schema.isEmpty()) {
             final String catalog = catalogOfSpatialTables;
@@ -581,23 +582,46 @@ public class Database<G> extends Syntax  {
     }
 
     /**
-     * Returns {@code true} if this database contains at least one geometry 
column.
-     * This information can be used for metadata purpose.
+     * If a <abbr>CRS</abbr> is available in the cache for the given SRID, 
returns that <abbr>CRS</abbr>.
+     * Otherwise returns {@code null}. This method does not query the database.
+     *
+     * @param  srid  identifier of the <abbr>CRS</abbr> to get.
+     * @return the requested <abbr>CRS</abbr>, or {@code null} if not in the 
cache.
+     */
+    public final CoordinateReferenceSystem getCachedCRS(final int srid) {
+        return cacheOfCRS.get(srid);
+    }
+
+    /**
+     * If a <abbr>SRID</abbr> is available in the cache for the given 
<abbr>CRS</abbr>, returns that SRID.
+     * Otherwise returns {@code null}. This method does not query the database.
      *
-     * @return whether at least one geometry column has been found.
+     * @param  crs  the <abbr>CRS</abbr> for which to get the 
<abbr>SRID</abbr>.
+     * @return the <abbr>SRID</abbr> for the given <abbr>CRS</abbr>, or {@code 
null} if not in the cache.
      */
-    public final boolean hasGeometry() {
-        return hasGeometry;
+    public final int getCachedSRID(final CoordinateReferenceSystem crs) {
+        synchronized (cacheOfSRID) {
+            return cacheOfSRID.get(crs);
+        }
     }
 
     /**
-     * Returns {@code true} if this database contains at least one raster 
column.
-     * This information can be used for metadata purpose.
+     * Returns a function for getting values from a geometry or geography 
column.
+     * This is a helper method for {@link #getMapping(Column)} implementations.
      *
-     * @return whether at least one raster column has been found.
+     * @param  columnDefinition  information about the column to extract 
values from and expose through Java API.
+     * @return converter to the corresponding java type, or {@code null} if 
this class cannot find a mapping,
      */
-    public final boolean hasRaster() {
-        return hasRaster;
+    protected final ValueGetter<?> forGeometry(final Column columnDefinition) {
+        /*
+         * The geometry type should not be empty. But it may still happen if 
the "GEOMETRY_COLUMNS"
+         * table does not contain a line for the specified column. It is a 
server issue, but seems
+         * to happen sometimes.
+         */
+        final GeometryType type = 
columnDefinition.getGeometryType().orElse(GeometryType.GEOMETRY);
+        final Class<? extends G> geometryClass = 
geomLibrary.getGeometryClass(type).asSubclass(geomLibrary.rootClass);
+        return new GeometryGetter<>(geomLibrary, geometryClass, 
columnDefinition.getDefaultCRS().orElse(null),
+                                    getBinaryEncoding(columnDefinition));
     }
 
     /**
@@ -663,6 +687,18 @@ public class Database<G> extends Syntax  {
         }
     }
 
+    /**
+     * Returns a mapping for {@link Types#JAVA_OBJECT} or unrecognized types. 
Some JDBC drivers wrap
+     * objects in implementation-specific classes, for example {@link 
org.postgresql.util.PGobject}.
+     * This method should be overwritten in database-specific subclasses for 
returning a value getter
+     * capable to unwrap the value.
+     *
+     * @return the default mapping for unknown or unrecognized types.
+     */
+    protected ValueGetter<Object> getDefaultMapping() {
+        return ValueGetter.AsObject.INSTANCE;
+    }
+
     /**
      * Returns the type of components in SQL arrays stored in a column.
      * This method is invoked when {@link Column#type} = {@link Types#ARRAY}.
@@ -680,18 +716,6 @@ public class Database<G> extends Syntax  {
         return Types.OTHER;
     }
 
-    /**
-     * Returns a mapping for {@link Types#JAVA_OBJECT} or unrecognized types. 
Some JDBC drivers wrap
-     * objects in implementation-specific classes, for example {@link 
org.postgresql.util.PGobject}.
-     * This method should be overwritten in database-specific subclasses for 
returning a value getter
-     * capable to unwrap the value.
-     *
-     * @return the default mapping for unknown or unrecognized types.
-     */
-    protected ValueGetter<Object> getDefaultMapping() {
-        return ValueGetter.AsObject.INSTANCE;
-    }
-
     /**
      * Returns an identifier of the way binary data are encoded by the JDBC 
driver.
      *
@@ -719,36 +743,6 @@ public class Database<G> extends Syntax  {
         return null;
     }
 
-    /**
-     * Returns a function for getting values from a geometry or geography 
column.
-     * This is a helper method for {@link #getMapping(Column)} implementations.
-     *
-     * @param  columnDefinition  information about the column to extract 
values from and expose through Java API.
-     * @return converter to the corresponding java type, or {@code null} if 
this class cannot find a mapping,
-     */
-    protected final ValueGetter<?> forGeometry(final Column columnDefinition) {
-        /*
-         * The geometry type should not be empty. But it may still happen if 
the "GEOMETRY_COLUMNS"
-         * table does not contain a line for the specified column. It is a 
server issue, but seems
-         * to happen sometimes.
-         */
-        final GeometryType type = 
columnDefinition.getGeometryType().orElse(GeometryType.GEOMETRY);
-        final Class<? extends G> geometryClass = 
geomLibrary.getGeometryClass(type).asSubclass(geomLibrary.rootClass);
-        return new GeometryGetter<>(geomLibrary, geometryClass, 
columnDefinition.getDefaultCRS().orElse(null),
-                                    getBinaryEncoding(columnDefinition));
-    }
-
-    /**
-     * Prepares a cache of statements about spatial information using the 
given connection.
-     * Statements will be created only when first needed.
-     *
-     * @param  connection  the connection to use for creating statements.
-     * @return a cache of prepared statements about spatial information.
-     */
-    protected InfoStatements createInfoStatements(final Connection connection) 
{
-        return new InfoStatements(this, connection);
-    }
-
     /**
      * Returns the spatial schema conventions that may possibly be supported 
by this database.
      * The default implementation returns all {@link SpatialSchema} 
enumeration values.
@@ -802,6 +796,17 @@ public class Database<G> extends Syntax  {
         return filterToSQL;
     }
 
+    /**
+     * Prepares a cache of statements about spatial information using the 
given connection.
+     * Each statement in the returned object will be created only when first 
needed.
+     *
+     * @param  connection  the connection to use for creating statements.
+     * @return a cache of prepared statements about spatial information.
+     */
+    public InfoStatements createInfoStatements(final Connection connection) {
+        return new InfoStatements(this, connection);
+    }
+
     /**
      * Sets the logger, class and method names of the given record, then logs 
it.
      * This method declares {@link SQLStore#components()} as the public source 
of the log.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
index 19adc0d1f7..8df07b0a93 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
@@ -20,19 +20,19 @@ import java.util.Map;
 import java.util.Set;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
-import java.util.AbstractMap.SimpleImmutableEntry;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import java.util.concurrent.Callable;
 import java.text.ParseException;
-import java.sql.Array;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.Statement;
 import java.sql.SQLException;
+import org.opengis.util.FactoryException;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.crs.CRSAuthorityFactory;
@@ -41,12 +41,15 @@ import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.crs.AbstractCRS;
+import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
 import org.apache.sis.referencing.privy.DefinitionVerifier;
 import org.apache.sis.referencing.privy.ReferencingUtilities;
 import org.apache.sis.metadata.sql.privy.SQLUtilities;
 import org.apache.sis.metadata.sql.privy.SQLBuilder;
 import org.apache.sis.geometry.wrapper.GeometryType;
+import org.apache.sis.system.CommonExecutor;
 import org.apache.sis.system.Modules;
 import org.apache.sis.util.Localized;
 import org.apache.sis.util.Utilities;
@@ -95,7 +98,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * cache of CRS created from SRID codes and the listeners where to send 
warnings.
      * A {@code Database} object does <strong>not</strong> contain live JDBC 
{@link Connection}.
      */
-    private final Database<?> database;
+    protected final Database<?> database;
 
     /**
      * Connection to use for creating the prepared statements.
@@ -147,24 +150,12 @@ public class InfoStatements implements Localized, 
AutoCloseable {
         return database.listeners.getLocale();
     }
 
-    /**
-     * Returns a function for getting values of components in the given array.
-     * If no match is found, then this method returns {@code null}.
-     *
-     * @param  array  the array from which to get the mapping of component 
values.
-     * @return converter to the corresponding java type, or {@code null} if 
this class cannot find a mapping.
-     * @throws SQLException if the mapping cannot be obtained.
-     */
-    public final ValueGetter<?> getComponentMapping(final Array array) throws 
SQLException {
-        return database.getMapping(new Column(array.getBaseType(), 
array.getBaseTypeName()));
-    }
-
     /**
      * Appends a {@code " FROM <table> WHERE "} text to the given builder.
      * The table name will be prefixed by catalog and schema name if 
applicable.
      */
     private void appendFrom(final SQLBuilder sql, final String table) {
-        database.appendSpatialSchema(sql.append(" FROM "), table);
+        database.formatTableName(sql.append(" FROM "), table);
         sql.append(" WHERE ");
     }
 
@@ -293,6 +284,8 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      *         or a single entry exists but has no WKT definition and its 
authority code is unsupported by SIS.
      * @throws ParseException if the WKT cannot be parsed.
      * @throws SQLException if a SQL error occurred.
+     *
+     * @see org.apache.sis.storage.sql.SQLStore#findCRS(int)
      */
     public final CoordinateReferenceSystem fetchCRS(final int srid) throws 
Exception {
         /*
@@ -340,6 +333,9 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * {@link SpatialSchema#refSysTable} then gets the CRS from its authority 
code if possible,
      * or fallback on the WKT otherwise.
      *
+     * <p>This method does not cache the returned <abbr>CRS</abbr>.
+     * The caching is done by {@code Cache.getOrCreate(…)}.</p>
+     *
      * @param  srid  the Spatial Reference Identifier (SRID) of the CRS to 
create from the database content.
      * @return the CRS created from database content.
      * @throws Exception if an SQL error, parsing error or other error 
occurred.
@@ -476,17 +472,148 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * @param  crs     the CRS for which to find a SRID, or {@code null}.
      * @return SRID for the given CRS, or 0 if the given CRS was null.
      * @throws Exception if an SQL error, parsing error or other error 
occurred.
+     *
+     * @see 
org.apache.sis.storage.sql.SQLStore#findSRID(CoordinateReferenceSystem)
      */
     public final int findSRID(final CoordinateReferenceSystem crs) throws 
Exception {
         if (crs == null) {
             return 0;
         }
+        final SRID result;
+        final Integer srid;
         synchronized (database.cacheOfSRID) {
-            final Integer srid = database.cacheOfSRID.get(crs);
-            if (srid != null) {
-                return srid;
+            final Integer cached = database.cacheOfSRID.get(crs);
+            if (cached != null) {
+                return cached;
+            }
+            result = findOrAddCRS(crs);
+            database.cacheOfSRID.put(crs, srid = result.srid);
+        }
+        CommonExecutor.instance().submit((Runnable) result);
+        return result.srid;
+    }
+
+    /**
+     * The result of a search for the <abbr>SRID</abbr> from a 
<abbr>CRS</abbr>.
+     * This result is stored in the {@link #srid} field, but is accompanied by 
information allowing to
+     * opportunistically cache the result in the map for the opposite search 
(fetching <abbr>CRS</abbr>
+     * from a <abbr>SRID</abbr>). A difficulty is that the opposite operation 
would query the geodetic
+     * registry for more metadata. So in order to have more consistent values 
in the cache, we need to
+     * do the same query in {@link #call()} as what would have been done with 
{@code fetchCRS(srid)}.
+     */
+    private final class SRID implements Callable<CoordinateReferenceSystem>, 
Runnable {
+        /**
+         * The <abbr>CRS</abbr> for which to provide an "authority:code pair".
+         */
+        final CoordinateReferenceSystem crs;
+
+        /**
+         * The authority managing the code value.
+         * This is part of the key in hash map.
+         */
+        final String authority;
+
+        /**
+         * Code managed by the authority.
+         * This is part of the key in hash map.
+         */
+        final int code;
+
+        /**
+         * Primary key used in the {@code SPATIAL_REF_SYS} table.
+         * This is initially zero, then set to a value after the 
<abbr>SRID</abbr> has been computed.
+         */
+        int srid;
+
+        /**
+         * Creates a new "authority:code" pair for holding the 
<abbr>SRID</abbr> result.
+         */
+        SRID(final CoordinateReferenceSystem crs, final String authority, 
final int code) {
+            this.crs       = crs;
+            this.authority = authority;
+            this.code      = code;
+        }
+
+        /**
+         * Invoked in a background thread for opportunistically caching the 
<abbr>SRID</abbr> → <abbr>CRS</abbr>
+         * mapping. This is done in a background thread because this is not 
the information asked by the user,
+         * by the information for the reverse operation.
+         */
+        @Override
+        public void run() {
+            try {
+                database.cacheOfCRS.getOrCreate(srid, this);
+            } catch (Exception e) {
+                log("findSRID", new LogRecord(Level.FINER, e.toString()));
             }
         }
+
+        /**
+         * Simulates a call to {@code fetchCRS(srid)} for cache consistency.
+         */
+        @Override
+        public CoordinateReferenceSystem call() throws FactoryException {
+            final CoordinateReferenceSystem fromAuthority;
+            try {
+                final CRSAuthorityFactory factory = 
CRS.getAuthorityFactory(authority);
+                fromAuthority = 
factory.createCoordinateReferenceSystem(Integer.toString(code));
+            } catch (NoSuchAuthorityCodeException e) {
+                return crs;
+            }
+            for (int i=0; ; i++) {
+                final CoordinateReferenceSystem candidate;
+                switch (i) {
+                    case 0: candidate = fromAuthority; break;
+                    case 1: candidate = 
AbstractCRS.castOrCopy(fromAuthority).forConvention(AxesConvention.RIGHT_HANDED);
 break;
+                    case 2: candidate = 
AbstractCRS.castOrCopy(fromAuthority).forConvention(AxesConvention.DISPLAY_ORIENTED);
 break;
+                    default: return crs;
+                }
+                if (Utilities.equalsApproximately(crs, candidate)) {
+                    return candidate;
+                }
+            }
+        }
+
+        /**
+         * Compares the "authority:code" tuples, intentionally omitting other 
properties.
+         * Only the "authority:code" pair is compared. Needed for use in hash 
map,
+         * in order to avoid processing the same "authority:code" pair many 
times.
+         */
+        @Override
+        public boolean equals(final Object obj) {
+            if (obj instanceof SRID) {
+                final var other = (SRID) obj;
+                return other.code == code && authority.equals(other.authority);
+            }
+            return false;
+        }
+
+        /**
+         * Returns a hash code compatible with {@link #equals'Object)} 
contract.
+         */
+        @Override
+        public int hashCode() {
+            return authority.hashCode() + code;
+        }
+
+        /**
+         * Returns a string representation for debugging purposes.
+         */
+        @Override
+        public String toString() {
+            return authority + ':' + code + " → " + srid;
+        }
+    }
+
+    /**
+     * Invoked when a <abbr>CRS</abbr> is not in the cache.
+     * This method does not cache the result. Caching should be done by the 
caller.
+     *
+     * @param  crs  the <abbr>CRS</abbr> to search.
+     * @return <abbr>SRID</abbr> associated to the given <abbr>CRS</abbr>.
+     * @throws Exception if an SQL error, parsing error or other error 
occurred.
+     */
+    private SRID findOrAddCRS(final CoordinateReferenceSystem crs) throws 
Exception {
         /*
          * Search in the database. In the `done` map, keys are 
"authority:code" pairs that we
          * already tried and values tell whether we can use that pair if we 
need to add new CRS.
@@ -494,7 +621,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
         Exception error = null;
         boolean tryWithGivenCRS = true;
         final var sridFounInUse = new HashSet<Integer>();
-        final var done = new 
LinkedHashMap<SimpleImmutableEntry<String,Object>, Boolean>();
+        final var done = new LinkedHashMap<SRID, Boolean>();    // Value tells 
whether the SRID may be valid.
         for (Iterator<IdentifiedObject> it = 
Set.<IdentifiedObject>of(crs).iterator(); it.hasNext();) {
             final IdentifiedObject candidate = it.next();
             /*
@@ -505,21 +632,19 @@ public class InfoStatements implements Localized, 
AutoCloseable {
             for (final Identifier id : candidate.getIdentifiers()) {
                 final String authority = id.getCodeSpace();
                 if (authority == null) continue;
-                final String code = id.getCode();
-                if (done.putIfAbsent(new SimpleImmutableEntry<>(authority, 
code), Boolean.FALSE) != null) {
-                    continue;                           // Skip 
"authority:code" that we already tried.
-                }
-                final int codeValue;
+                final int code;
                 try {
-                    codeValue = Integer.parseInt(code);
+                    code = Integer.parseInt(id.getCode());
                 } catch (NumberFormatException e) {
-                    if (error == null) error = e;
-                    else error.addSuppressed(e);
+                    if (tryWithGivenCRS) {
+                        if (error == null) error = e;
+                        else error.addSuppressed(e);
+                    }
                     continue;                           // Ignore codes that 
are not integers.
                 }
-                final var key = new 
SimpleImmutableEntry<String,Object>(authority, codeValue);
-                if (done.putIfAbsent(key, codeValue > 0) != null) {
-                    continue;
+                final SRID search = new SRID(crs, authority, code);
+                if (done.putIfAbsent(search, code > 0) != null) {
+                    continue;                           // Skip 
"authority:code" that we already tried.
                 }
                 /*
                  * Found an "authority:code" pair that we did not tested 
before.
@@ -528,7 +653,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
                 if (sridFromCRS == null) {
                     sridFromCRS = prepareSearchCRS(true);
                 }
-                sridFromCRS.setInt(1, codeValue);
+                sridFromCRS.setInt(1, code);
                 sridFromCRS.setString(2, SQLUtilities.toLikePattern(authority, 
true));
                 try (ResultSet result = sridFromCRS.executeQuery()) {
                     while (result.next()) {
@@ -537,16 +662,14 @@ public class InfoStatements implements Localized, 
AutoCloseable {
                             if (sridFounInUse.add(srid)) try {
                                 final Object parsed = parseDefinition(result, 
3);
                                 if (Utilities.equalsApproximately(parsed, 
crs)) {
-                                    synchronized (database.cacheOfSRID) {
-                                        database.cacheOfSRID.put(crs, srid);
-                                    }
-                                    return srid;
+                                    search.srid = srid;
+                                    return search;
                                 }
                             } catch (ParseException e) {
                                 if (error == null) error = e;
                                 else error.addSuppressed(e);
                             }
-                            done.put(key, Boolean.FALSE);       // Declare 
this "authority:code" pair as not available.
+                            done.put(search, Boolean.FALSE);    // Declare 
this "authority:code" pair as not available.
                         }
                     }
                 }
@@ -569,24 +692,22 @@ public class InfoStatements implements Localized, 
AutoCloseable {
          * It is caller's responsibility to hold a write lock if needed.
          */
         if (!connection.isReadOnly()) {
-            String fallback = null;
-            int fallbackCode = 0;
-            for (final Map.Entry<SimpleImmutableEntry<String,Object>, Boolean> 
entry : done.entrySet()) {
+            SRID fallback = null;
+            for (final Map.Entry<SRID, Boolean> entry : done.entrySet()) {
                 if (entry.getValue()) {
-                    final SimpleImmutableEntry<String,Object> identifier = 
entry.getKey();
-                    final int code = (Integer) identifier.getValue();  // Safe 
cast when `entry.getValue()` is true.
-                    final String authority = identifier.getKey();
-                    if (Constants.EPSG.equalsIgnoreCase(authority)) {
-                        return addCRS(authority, code, crs, sridFounInUse);
+                    final SRID search = entry.getKey();
+                    if (Constants.EPSG.equalsIgnoreCase(search.authority)) {
+                        search.srid = addCRS(search, sridFounInUse);
+                        return search;
                     }
                     if (fallback == null) {
-                        fallback = authority;
-                        fallbackCode = code;
+                        fallback = search;
                     }
                 }
             }
             if (fallback != null) {
-                return addCRS(fallback, fallbackCode, crs, sridFounInUse);
+                fallback.srid = addCRS(fallback, sridFounInUse);
+                return fallback;
             }
             // In the current version, we don't encode a CRS without an 
"authority:code" pair.
         }
@@ -599,22 +720,19 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * Adds a new entry in the {@code SPATIAL_REF_SYS} table for the given 
<abbr>CRS</abbr>.
      * The {@code authority} and {@code code} arguments are the values to 
store in the {@code "AUTH_NAME"} and
      * {@code "AUTH_SRID"} columns respectively. A common usage is to prefer 
EPSG codes, but this is not mandatory.
+     * This method does not cache the added <abbr>CRS</abbr>. Caching should 
be done by the caller.
      *
-     * @param  authority      authority providing the CRS definition.
-     * @param  code           authority code.
-     * @param  crs            the coordinate reference system to encode.
+     * @param  search         the <abbr>CRS</abbr> to search together with its 
"authority:code" pair.
      * @param  sridFounInUse  SRIDs which have been found in use, for avoiding 
SRID that would be certain to fail.
      *                        This is only a hint. A SRID not in this set is 
not a guarantee that it is available.
      * @return SRID of the CRS added by this method.
      * @throws DataStoreReferencingException if the given CRS cannot be 
encoded in at least one supported format.
      * @throws SQLException if an error occurred while executing the SQL 
statement.
      */
-    private int addCRS(final String authority, final int code, final 
CoordinateReferenceSystem crs,
-            final Set<Integer> sridFounInUse) throws Exception
-    {
+    private int addCRS(final SRID search, final Set<Integer> sridFounInUse) 
throws Exception {
         final SpatialSchema schema = database.getSpatialSchema().orElseThrow();
         final var sql = new SQLBuilder(database).append(SQLBuilder.INSERT);
-        database.appendSpatialSchema(sql, schema.crsTable);
+        database.formatTableName(sql, schema.crsTable);
         sql.append(" (").append(schema.crsIdentifierColumn)
            .append(", ").append(schema.crsAuthorityNameColumn)
            .append(", ").append(schema.crsAuthorityCodeColumn);
@@ -633,11 +751,11 @@ public class InfoStatements implements Localized, 
AutoCloseable {
             final String def;
             switch (encoding) {
                 default: continue;          // Skip unknown formats (none at 
this time).
-                case WKT1: def = wktFormat.format(crs); break;
+                case WKT1: def = wktFormat.format(search.crs); break;
                 case WKT2: {
                     try {
                         wktFormat.setConvention(Convention.WKT2);
-                        def = wktFormat.format(crs);
+                        def = wktFormat.format(search.crs);
                     } finally {
                         wktFormat.setConvention(Convention.WKT1_COMMON_UNITS);
                     }
@@ -671,8 +789,8 @@ public class InfoStatements implements Localized, 
AutoCloseable {
         do {
             final String column = description ? schema.crsDescriptionColumn : 
schema.crsNameColumn;
             if (column != null) {
-                String name = description ? 
IdentifiedObjects.getDisplayName(crs, getLocale())
-                                          : IdentifiedObjects.getName(crs, 
null);
+                String name = description ? 
IdentifiedObjects.getDisplayName(search.crs, getLocale())
+                                          : 
IdentifiedObjects.getName(search.crs, null);
                 if (name != null) {
                     definitions[numDefinitions++] = name;
                     sql.append(", ").append(column);
@@ -689,10 +807,10 @@ public class InfoStatements implements Localized, 
AutoCloseable {
             sql.append(separator).append('?');
             separator = ", ";
         }
-        int srid = code;
+        int srid = search.code;
         try (PreparedStatement stmt = 
connection.prepareStatement(sql.append(')').toString())) {
-            stmt.setString(2, authority);
-            stmt.setInt(3, code);
+            stmt.setString(2, search.authority);
+            stmt.setInt(3, search.code);
             for (int i=0; i<numDefinitions; i++) {
                 stmt.setString(4+i, definitions[i]);
             }
@@ -715,7 +833,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
                  */
                 try {
                     final CoordinateReferenceSystem candidate = fetchCRS(srid);
-                    if (Utilities.equalsIgnoreMetadata(crs, candidate)) {
+                    if (Utilities.equalsIgnoreMetadata(search.crs, candidate)) 
{
                         return srid;
                     }
                 } catch (Exception f) {
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SchemaModifier.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SchemaModifier.java
index 46a1b40865..fe204d2705 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SchemaModifier.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SchemaModifier.java
@@ -71,5 +71,5 @@ public interface SchemaModifier {
      * @todo if we move this key in public API in the future, then it would be 
a
      *       value in existing {@link org.apache.sis.storage.DataOptionKey} 
class.
      */
-    OptionKey<SchemaModifier> OPTION = new 
InternalOptionKey<SchemaModifier>("SCHEMA_MODIFIER", SchemaModifier.class);
+    OptionKey<SchemaModifier> OPTION = new 
InternalOptionKey<>("SCHEMA_MODIFIER", SchemaModifier.class);
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java
index b1158d119d..dd1253b117 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/ValueGetter.java
@@ -487,7 +487,11 @@ public class ValueGetter<T> {
         }
         Object result = array.getArray();
         if (cmget == null && stmts != null) {
-            cmget = stmts.getComponentMapping(array);
+            /*
+             * Get a function for getting values of components in the array.
+             * If no match is found, then `cmget` stay null.
+             */
+            cmget = stmts.database.getMapping(new Column(array.getBaseType(), 
array.getBaseTypeName()));
         }
         Class<?> componentType = 
Numbers.primitiveToWrapper(result.getClass().getComponentType());
         if (cmget != null && !cmget.valueType.isAssignableFrom(componentType)) 
{
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java
index d9680f2620..5865e933c1 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java
@@ -120,7 +120,7 @@ final class ExtentEstimator {
     private void query(final Statement statement) throws SQLException {
         for (final Column column : columns) {
             if (column.getGeometryType().isPresent()) {
-                
database.appendSpatialSchema(builder.append(SQLBuilder.SELECT), 
"ST_EstimatedExtent");
+                database.formatTableName(builder.append(SQLBuilder.SELECT), 
"ST_EstimatedExtent");
                 builder.append('(');
                 if (table.schema != null) {
                     builder.appendValue(table.schema).append(", ");
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java
index 20a4a43d1a..d8ad170df1 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/Postgres.java
@@ -18,6 +18,8 @@ package org.apache.sis.storage.sql.postgis;
 
 import java.util.Map;
 import java.util.Locale;
+import java.util.logging.Level;
+import java.util.concurrent.locks.ReadWriteLock;
 import java.sql.Types;
 import java.sql.JDBCType;
 import java.sql.Connection;
@@ -26,7 +28,6 @@ import java.sql.ResultSet;
 import java.sql.DatabaseMetaData;
 import java.sql.SQLException;
 import javax.sql.DataSource;
-import java.util.logging.Level;
 import org.opengis.geometry.Envelope;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.storage.sql.feature.BinaryEncoding;
@@ -72,13 +73,15 @@ public final class Postgres<G> extends Database<G> {
      * @param  dialect      additional information not provided by {@code 
metadata}.
      * @param  geomLibrary  the factory to use for creating geometric objects.
      * @param  listeners    where to send warnings.
+     * @param  locks        the read/write locks, or {@code null} if none.
      * @throws SQLException if an error occurred while reading database 
metadata.
      */
     public Postgres(final DataSource source, final Connection connection, 
final DatabaseMetaData metadata,
-                    final Dialect dialect, final Geometries<G> geomLibrary, 
final StoreListeners listeners)
+                    final Dialect dialect, final Geometries<G> geomLibrary, 
final StoreListeners listeners,
+                    final ReadWriteLock locks)
             throws SQLException
     {
-        super(source, metadata, dialect, geomLibrary, listeners);
+        super(source, metadata, dialect, geomLibrary, listeners, locks);
         Version version = null;
         try (Statement st = connection.createStatement();
              ResultSet result = st.executeQuery("SELECT 
public.PostGIS_version();"))
@@ -181,7 +184,7 @@ public final class Postgres<G> extends Database<G> {
      * @return a cache of prepared statements about spatial information.
      */
     @Override
-    protected InfoStatements createInfoStatements(final Connection connection) 
{
+    public InfoStatements createInfoStatements(final Connection connection) {
         return new ExtendedInfo(this, connection);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/InfoStatementsTest.java
 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/InfoStatementsTest.java
index b93ac7b67a..403bdee386 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/InfoStatementsTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/InfoStatementsTest.java
@@ -95,7 +95,7 @@ public final class InfoStatementsTest extends TestCase {
         connection = test.source.getConnection();
         database = new Database<>(test.source, connection.getMetaData(), 
Dialect.DERBY,
                                   Geometries.factory(GeometryLibrary.JAVA2D),
-                                  new StoreListeners(null, new 
DataStoreMock("Unused")));
+                                  new StoreListeners(null, new 
DataStoreMock("Unused")), null);
         /*
          * The `spatialSchema` is private, so we need to use reflection for 
setting its value.
          * Normally that field would be set by `Database.analyze(…)`, bur we 
want to avoid that

Reply via email to