You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/03/15 19:36:51 UTC

[sis] 02/02: Make SQLStore a little bit more robust to NullPointerException when the geometry type can not be determined. Add support for arrays, which are implemented as multi-occurrence attribute values in features. Unwrap the value of `org.postgresql.util.PGobject`.

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 c5263872c941d2735894064cf73ef9031d223f73
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Mar 15 20:03:01 2022 +0100

    Make SQLStore a little bit more robust to NullPointerException when the geometry type can not be determined.
    Add support for arrays, which are implemented as multi-occurrence attribute values in features.
    Unwrap the value of `org.postgresql.util.PGobject`.
---
 .../apache/sis/internal/feature/GeometryType.java  |  2 +-
 ide-project/NetBeans/nbproject/project.properties  |  2 +-
 storage/sis-sqlstore/pom.xml                       |  2 +-
 .../apache/sis/internal/sql/feature/Analyzer.java  |  2 +-
 .../apache/sis/internal/sql/feature/Column.java    | 73 ++++++++++++++-----
 .../apache/sis/internal/sql/feature/Database.java  | 53 ++++++++++++--
 .../sis/internal/sql/feature/InfoStatements.java   | 13 ++++
 .../sis/internal/sql/feature/ValueGetter.java      | 81 +++++++++++++++++++++-
 .../sis/internal/sql/postgis/ExtentEstimator.java  |  4 +-
 .../sis/internal/sql/postgis/ObjectGetter.java     | 76 ++++++++++++++++++++
 .../apache/sis/internal/sql/postgis/Postgres.java  | 24 ++++++-
 11 files changed, 301 insertions(+), 31 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryType.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryType.java
index a326c43..dbc7746 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryType.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryType.java
@@ -145,7 +145,7 @@ public enum GeometryType {
      * Types for geometries having <var>Z</var> and <var>M</var> are replaced by 2D types.
      *
      * @param  type  WKB geometry type.
-     * @return enumeration value for the given type, or {@code null}.
+     * @return enumeration value for the given type, or {@code null} if the given type is not recognized.
      *
      * @see #binaryType()
      */
diff --git a/ide-project/NetBeans/nbproject/project.properties b/ide-project/NetBeans/nbproject/project.properties
index bc3ac69..7a573de 100644
--- a/ide-project/NetBeans/nbproject/project.properties
+++ b/ide-project/NetBeans/nbproject/project.properties
@@ -140,6 +140,7 @@ javac.classpath=\
     ${maven.repository}/com/esri/geometry/esri-geometry-api/${esri.api.version}/esri-geometry-api-${esri.api.version}.jar:\
     ${maven.repository}/org/locationtech/jts/jts-core/${jts.version}/jts-core-${jts.version}.jar:\
     ${maven.repository}/javax/javaee-api/${jee.version}/javaee-api-${jee.version}.jar:\
+    ${maven.repository}/org/postgresql/postgresql/${postgresql.version}/postgresql-${postgresql.version}.jar:\
     ${maven.repository}/edu/ucar/cdm-core/${netcdf.version}/cdm-core-${netcdf.version}.jar:\
     ${maven.repository}/edu/ucar/udunits/${netcdf.version}/udunits-${netcdf.version}.jar:\
     ${maven.repository}/com/google/guava/guava/${guava.version}/guava-${guava.version}.jar:\
@@ -149,7 +150,6 @@ javac.processorpath=\
 javac.test.classpath=\
     ${javac.classpath}:\
     ${maven.repository}/org/apache/derby/derby/${derby.version}/derby-${derby.version}.jar:\
-    ${maven.repository}/org/postgresql/postgresql/${postgresql.version}/postgresql-${postgresql.version}.jar:\
     ${maven.repository}/org/hsqldb/hsqldb/${hsqldb.version}/hsqldb-${hsqldb.version}.jar:\
     ${maven.repository}/com/h2database/h2/${h2.version}/h2-${h2.version}.jar:\
     ${maven.repository}/gov/nist/math/jama/${jama.version}/jama-${jama.version}.jar:\
diff --git a/storage/sis-sqlstore/pom.xml b/storage/sis-sqlstore/pom.xml
index 3053c18..cf1f6f3 100644
--- a/storage/sis-sqlstore/pom.xml
+++ b/storage/sis-sqlstore/pom.xml
@@ -142,7 +142,7 @@
     <dependency>
       <groupId>org.postgresql</groupId>
       <artifactId>postgresql</artifactId>
-      <scope>test</scope>
+      <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.locationtech.jts</groupId>
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 2a288ea..9e84bed 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
@@ -272,7 +272,7 @@ final class Analyzer {
     final ValueGetter<?> setValueGetter(final Column column) {
         ValueGetter<?> getter = database.getMapping(column);
         if (getter == null) {
-            getter = ValueGetter.AsObject.INSTANCE;
+            getter = database.getDefaultMapping();
             warning(Resources.Keys.UnknownType_1, column.typeName);
         }
         column.valueGetter = getter;
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Column.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Column.java
index 1ec8fbe..b2dbd83 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Column.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Column.java
@@ -21,6 +21,8 @@ import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
 import java.sql.DatabaseMetaData;
 import java.sql.SQLException;
+import java.sql.SQLDataException;
+import java.util.Optional;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.internal.metadata.sql.Reflection;
 import org.apache.sis.internal.metadata.sql.SQLUtilities;
@@ -45,7 +47,7 @@ import org.apache.sis.util.Localized;
  *
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see ResultSet#getMetaData()
  * @see DatabaseMetaData#getColumns(String, String, String, String)
@@ -81,8 +83,9 @@ public final class Column {
     public final int type;
 
     /**
-     * A name for the value type, free-text from the database engine. For more information about this, please see
+     * A name for the value type, free-text from the database engine. For more information about this, see
      * {@link DatabaseMetaData#getColumns(String, String, String, String)} and {@link Reflection#TYPE_NAME}.
+     * This value shall not be null.
      *
      * @see Reflection#TYPE_NAME
      */
@@ -127,6 +130,21 @@ public final class Column {
     ValueGetter<?> valueGetter;
 
     /**
+     * Creates a synthetic column (a column not inferred from database analysis)
+     * for describing the type of elements in an array.
+     *
+     * @param  type      SQL type of the column.
+     * @param  typeName  SQL name of the type.
+     */
+    Column(final int type, final String typeName) {
+        this.name = label = propertyName = "element";
+        this.type       = type;
+        this.typeName   = typeName;
+        this.precision  = 0;
+        this.isNullable = false;
+    }
+
+    /**
      * Creates a new column from database metadata.
      * Information are fetched from current {@code ResultSet} row.
      * This method does not change cursor position.
@@ -171,9 +189,16 @@ public final class Column {
      * PostgreSQL JDBC drivers sometime gives the fully qualified type name.
      * For example we sometime get {@code "public"."geometry"} (including the quotes)
      * instead of a plain {@code geometry}. If this is the case, keep only the local part.
+     *
+     * @param  type   value found in the {@value Reflection.TYPE_NAME} column.
+     * @param  quote  value of {@code DatabaseMetaData.getIdentifierQuoteString()}.
+     * @return local part of the type name.
      */
-    private static String localPart(String type, final String quote) {
-        if (type != null && quote != null) {
+    private static String localPart(String type, final String quote) throws SQLDataException {
+        if (type == null) {
+            throw new SQLDataException(Errors.format(Errors.Keys.MissingValueInColumn_1, Reflection.TYPE_NAME));
+        }
+        if (quote != null) {
             int end = type.lastIndexOf(quote);
             if (end >= 0) {
                 int start = type.lastIndexOf(quote, end - 1);
@@ -214,24 +239,24 @@ public final class Column {
 
     /**
      * If this column is a geometry column, returns the type of the geometry objects.
-     * Otherwise returns {@code null} (including the case where this is a raster column).
+     * Otherwise returns empty (including the case where this is a raster column).
      * Note that if this column is a geometry column but the geometry type was not defined,
      * then {@link GeometryType#GEOMETRY} is returned as a fallback.
      *
-     * @return type of geometry objects, or {@code null} if this column is not a geometry column.
+     * @return type of geometry objects, or empty if this column is not a geometry column.
      */
-    public final GeometryType getGeometryType() {
-        return geometryType;
+    public final Optional<GeometryType> getGeometryType() {
+        return Optional.ofNullable(geometryType);
     }
 
     /**
      * If this column is a geometry or raster column, returns the default coordinate reference system.
-     * Otherwise returns {@code null}. The CRS may also be null even for a geometry column if it is unspecified.
+     * Otherwise returns empty. The CRS may also be empty even for a geometry column if it is unspecified.
      *
-     * @return CRS of geometries or rasters in this column, or {@code null} if unknown or not applicable.
+     * @return CRS of geometries or rasters in this column, or empty if unknown or not applicable.
      */
-    public final CoordinateReferenceSystem getDefaultCRS() {
-        return defaultCRS;
+    public final Optional<CoordinateReferenceSystem> getDefaultCRS() {
+        return Optional.ofNullable(defaultCRS);
     }
 
     /**
@@ -242,15 +267,29 @@ public final class Column {
      * @return builder for the added feature attribute.
      */
     final AttributeTypeBuilder<?> createAttribute(final FeatureTypeBuilder feature) {
-        final Class<?> type = valueGetter.valueType;
-        final AttributeTypeBuilder<?> attribute = feature.addAttribute(type).setName(propertyName);
-        if (precision > 0 && CharSequence.class.isAssignableFrom(type)) {
+        Class<?> valueType = valueGetter.valueType;
+        final boolean isArray = (valueGetter instanceof ValueGetter.AsArray);
+        if (isArray) {
+            valueType = ((ValueGetter.AsArray) valueGetter).cmget.valueType;
+        }
+        final AttributeTypeBuilder<?> attribute = feature.addAttribute(valueType).setName(propertyName);
+        if (precision > 0 && precision != Integer.MAX_VALUE && CharSequence.class.isAssignableFrom(valueType)) {
             attribute.setMaximalLength(precision);
         }
-        if (isNullable) {
+        if (isArray) {
+            /*
+             * We have no standard API yet for determining the minimal and maximal array length.
+             * The PostgreSQL driver seems to use the `precision` field, but it may be specific
+             * to that driver and seems to be always `MAX_VALUE` anyway.
+             */
             attribute.setMinimumOccurs(0);
+            attribute.setMaximumOccurs(Integer.MAX_VALUE);
+        } else if (isNullable) {
+            attribute.setMinimumOccurs(0);
+        }
+        if (geometryType != null || defaultCRS != null) {
+            attribute.setCRS(defaultCRS);
         }
-        attribute.setCRS(defaultCRS);
         return attribute;
     }
 
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 d00ff70..3abf514 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
@@ -27,6 +27,7 @@ import java.util.ArrayList;
 import java.util.Locale;
 import java.util.logging.LogRecord;
 import java.util.AbstractMap.SimpleImmutableEntry;
+import java.sql.Array;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
@@ -571,9 +572,8 @@ public class Database<G> extends Syntax  {
             case Types.TIME_WITH_TIMEZONE:        return ValueGetter.AsOffsetTime.INSTANCE;
             case Types.TIMESTAMP_WITH_TIMEZONE:   return ValueGetter.AsOffsetDateTime.INSTANCE;
             case Types.BLOB:                      return ValueGetter.AsBytes.INSTANCE;
-            case Types.ARRAY:                     // TODO
             case Types.OTHER:
-            case Types.JAVA_OBJECT:               return ValueGetter.AsObject.INSTANCE;
+            case Types.JAVA_OBJECT:               return getDefaultMapping();
             case Types.BINARY:
             case Types.VARBINARY:
             case Types.LONGVARBINARY: {
@@ -584,11 +584,48 @@ public class Database<G> extends Syntax  {
                     default: throw new AssertionError(encoding);
                 }
             }
+            case Types.ARRAY: {
+                final int componentType = getArrayComponentType(columnDefinition);
+                final ValueGetter<?> component = getMapping(new Column(componentType, columnDefinition.typeName));
+                if (component == ValueGetter.AsObject.INSTANCE) {
+                    return ValueGetter.AsArray.INSTANCE;
+                }
+                return new ValueGetter.AsArray(component);
+            }
             default: return null;
         }
     }
 
     /**
+     * Returns the type of components in SQL arrays stored in a column.
+     * This method is invoked when {@link #type} = {@link Types#ARRAY}.
+     * The default implementation returns {@link Types#OTHER} because JDBC
+     * column metadata does not provide information about component types.
+     * Database-specific subclasses should override this method if they can
+     * provide that information from the {@link Column#typeName} value.
+     *
+     * @param  columnDefinition  information about the column to extract array component type.
+     * @return one of {@link Types} constants.
+     *
+     * @see Array#getBaseType()
+     */
+    protected int getArrayComponentType(final Column columnDefinition) {
+        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.
      *
      * @param  columnDefinition  information about the column to extract binary values from.
@@ -616,16 +653,22 @@ public class Database<G> extends Syntax  {
     }
 
     /**
-     * Returns a function for getting values from a geometry column.
+     * 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 can not find a mapping,
      */
     protected final ValueGetter<?> forGeometry(final Column columnDefinition) {
-        final GeometryType type = columnDefinition.getGeometryType();
+        /*
+         * 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 sometime.
+         */
+        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(), getBinaryEncoding(columnDefinition));
+        return new GeometryGetter<>(geomLibrary, geometryClass, columnDefinition.getDefaultCRS().orElse(null),
+                                    getBinaryEncoding(columnDefinition));
     }
 
     /**
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/InfoStatements.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/InfoStatements.java
index 46a1e59..f04eca5 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/InfoStatements.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/InfoStatements.java
@@ -25,6 +25,7 @@ import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.text.ParseException;
+import java.sql.Array;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
@@ -178,6 +179,18 @@ public class InfoStatements implements Localized, AutoCloseable {
     }
 
     /**
+     * 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 can not find a mapping.
+     * @throws SQLException if the mapping can not 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.
      */
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ValueGetter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ValueGetter.java
index 99ae321..fd30c9a 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ValueGetter.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ValueGetter.java
@@ -17,6 +17,8 @@
 package org.apache.sis.internal.sql.feature;
 
 import java.util.Calendar;
+import java.util.Collection;
+import java.sql.Array;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Date;
@@ -28,7 +30,10 @@ import java.time.OffsetDateTime;
 import java.time.OffsetTime;
 import java.time.ZoneOffset;
 import java.math.BigDecimal;
+import org.apache.sis.math.Vector;
+import org.apache.sis.util.Numbers;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
 
 
 /**
@@ -97,8 +102,12 @@ public abstract class ValueGetter<T> {
         private AsObject() {super(Object.class);}
 
         /** Fetches the value from the specified column in the given result set. */
-        @Override public Object getValue(InfoStatements stmts, ResultSet source, int columnIndex) throws SQLException {
-            return source.getObject(columnIndex);
+        @Override public Object getValue(InfoStatements stmts, ResultSet source, int columnIndex) throws Exception {
+            Object value = source.getObject(columnIndex);
+            if (value instanceof Array) {
+                value = toCollection(stmts, null, (Array) value);
+            }
+            return value;
         }
     }
 
@@ -369,4 +378,72 @@ public abstract class ValueGetter<T> {
             return time.toLocalTime().atOffset(ZoneOffset.ofHoursMinutes(offsetMinute / 60, offsetMinute % 60));
         }
     }
+
+    /**
+     * A getter of values specified as Java array.
+     * This is okay for array of reasonable size.
+     * Should not be used for very large arrays.
+     */
+    static final class AsArray extends ValueGetter<Collection<?>> {
+        /** The getter for components in the array, or {@code null} for automatic. */
+        public final ValueGetter<?> cmget;
+
+        /** Accessor for components of automatic type. */
+        public static final AsArray INSTANCE = new AsArray(null);
+
+        /** Creates a new getter of arrays. */
+        @SuppressWarnings({"unchecked","rawtypes"})
+        AsArray(final ValueGetter<?> cmget) {
+            super((Class) Collection.class);
+            this.cmget = cmget;
+        }
+
+        /** Fetches the value from the specified column in the given result set. */
+        @Override public Collection<?> getValue(InfoStatements stmts, ResultSet source, int columnIndex) throws Exception {
+            return toCollection(stmts, cmget, source.getArray(columnIndex));
+        }
+    }
+
+    /**
+     * Converts the given SQL array to a Java array and free the SQL array.
+     * The returned array may be a primitive array or an array of objects.
+     *
+     * @param  stmts  information about the statement being executed, or {@code null} if none.
+     * @param  cmget  the getter for components in the array, or {@code null} for automatic.
+     * @param  array  the SQL array, or {@code null} if none.
+     * @return the Java array, or {@code null} if the given SQL array is null.
+     * @throws Exception if an error occurred. May be an SQL error, a WKB parsing error, <i>etc.</i>
+     */
+    protected static Collection<?> toCollection(final InfoStatements stmts, ValueGetter<?> cmget, final Array array) throws Exception {
+        if (array == null) {
+            return null;
+        }
+        Object result = array.getArray();
+        if (cmget == null && stmts != null) {
+            cmget = stmts.getComponentMapping(array);
+        }
+        Class<?> componentType = Numbers.primitiveToWrapper(result.getClass().getComponentType());
+        if (cmget != null && !cmget.valueType.isAssignableFrom(componentType)) {
+            /*
+             * If the elements in the `result` array are not of the expected type, fetch them again
+             * but this time using the converter. This fallback is inefficient because we fetch the
+             * same data that we already have, but the array should be short and this fallback will
+             * hopefully not be needed most of the time. It is also the only way to have the number
+             * of elements in advance.
+             */
+            componentType = Numbers.wrapperToPrimitive(cmget.valueType);
+            final int length = java.lang.reflect.Array.getLength(result);
+            result = java.lang.reflect.Array.newInstance(componentType, length);
+            try (ResultSet r = array.getResultSet()) {
+                while (r.next()) {
+                    java.lang.reflect.Array.set(result, r.getInt(1) - 1, cmget.getValue(stmts, r, 2));
+                }
+            }
+        }
+        array.free();
+        if (Numbers.isNumber(componentType)) {
+            return Vector.create(result, true);
+        }
+        return UnmodifiableArrayList.wrap((Object[]) result);
+    }
 }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtentEstimator.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtentEstimator.java
index 0cbbbd2..bf20180 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtentEstimator.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtentEstimator.java
@@ -123,7 +123,7 @@ final class ExtentEstimator {
      */
     private void query(final Statement statement) throws SQLException {
         for (final Column column : columns) {
-            if (column.getGeometryType() != null) {
+            if (column.getGeometryType().isPresent()) {
                 database.appendFunctionCall(builder.append("SELECT "), "ST_EstimatedExtent");
                 builder.append('(');
                 if (table.schema != null) {
@@ -136,7 +136,7 @@ final class ExtentEstimator {
                         final String wkt = result.getString(1);
                         if (wkt != null) {
                             final GeneralEnvelope env = new GeneralEnvelope(wkt);
-                            env.setCoordinateReferenceSystem(column.getDefaultCRS());
+                            column.getDefaultCRS().ifPresent(env::setCoordinateReferenceSystem);
                             if (envelope == null) {
                                 envelope = env;
                             } else try {
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ObjectGetter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ObjectGetter.java
new file mode 100644
index 0000000..b9f454a
--- /dev/null
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ObjectGetter.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.sql.postgis;
+
+import java.sql.Array;
+import java.sql.ResultSet;
+import org.apache.sis.internal.sql.feature.InfoStatements;
+import org.apache.sis.internal.sql.feature.ValueGetter;
+import org.postgresql.util.PGobject;
+
+
+/**
+ * Decoder of object of arbitrary kinds.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class ObjectGetter extends ValueGetter<Object> {
+    /**
+     * The singleton instance.
+     */
+    static final ObjectGetter INSTANCE = new ObjectGetter();
+
+    /**
+     * Creates the singleton instance.
+     */
+    private ObjectGetter() {
+        super(Object.class);
+    }
+
+    /**
+     * Gets the value in the column at specified index.
+     * The given result set must have its cursor position on the line to read.
+     * This method does not modify the cursor position.
+     *
+     * @param  stmts        prepared statements for fetching CRS from SRID, or {@code null} if none.
+     * @param  source       the result set from which to get the value.
+     * @param  columnIndex  index of the column in which to get the value.
+     * @return Object value in the given column. May be {@code null}.
+     * @throws Exception if an error occurred. May be an SQL error, a WKB parsing error, <i>etc.</i>
+     */
+    @Override
+    public Object getValue(InfoStatements stmts, ResultSet source, int columnIndex) throws Exception {
+        Object value = source.getObject(columnIndex);
+        if (value instanceof PGobject) {
+            final PGobject po = (PGobject) value;
+            /*
+             * TODO: we should invoke `getType()` and select a decoding algorithm depending on the type.
+             * The driver also has a `PGBinaryObject` that we can check for more efficient data transfer
+             * of points and bounding boxes. For now we just get the the wrapped value, which is always
+             * a `String`.
+             */
+            value = po.getValue();
+        }
+        if (value instanceof Array) {
+            value = toCollection(stmts, null, (Array) value);
+        }
+        return value;
+    }
+}
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/Postgres.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/Postgres.java
index 66a60b1..3c4ec10 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/Postgres.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/Postgres.java
@@ -119,12 +119,34 @@ public final class Postgres<G> extends Database<G> {
             return forGeometry(columnDefinition);
         }
         if ("raster".equalsIgnoreCase(columnDefinition.typeName)) {
-            return new RasterGetter(columnDefinition.getDefaultCRS(), getBinaryEncoding(columnDefinition));
+            return new RasterGetter(columnDefinition.getDefaultCRS().orElse(null),
+                                    getBinaryEncoding(columnDefinition));
         }
         return super.getMapping(columnDefinition);
     }
 
     /**
+     * Returns the type of components in SQL arrays stored in a column.
+     * This method is invoked when {@link #type} = {@link Types#ARRAY}.
+     */
+    @Override
+    protected int getArrayComponentType(final Column columnDefinition) {
+        switch (columnDefinition.typeName) {
+            // More types to be added later.
+            case "_text": return Types.VARCHAR;
+        }
+        return super.getArrayComponentType(columnDefinition);
+    }
+
+    /**
+     * Returns the mapping for {@link Object} or unrecognized types.
+     */
+    @Override
+    protected ValueGetter<Object> getDefaultMapping() {
+        return ObjectGetter.INSTANCE;
+    }
+
+    /**
      * Returns an identifier of the way binary data are encoded by the JDBC driver.
      * Data stored as PostgreSQL {@code BYTEA} type are encoded in hexadecimal.
      */