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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 8ee6f79  Make the `Gridcoverage` available as a `Feature` property. 
With this commit, PostGIS rasters are now available through the Feature API.
8ee6f79 is described below

commit 8ee6f794b2d3bc7467deaa8b4d46834a50387645
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Dec 25 21:56:38 2021 +0100

    Make the `Gridcoverage` available as a `Feature` property.
    With this commit, PostGIS rasters are now available through the Feature API.
---
 .../apache/sis/internal/sql/feature/Column.java    |  36 ++++---
 .../apache/sis/internal/sql/feature/Database.java  |  22 +++-
 .../sis/internal/sql/feature/FeatureAnalyzer.java  |  19 +++-
 .../sis/internal/sql/feature/GeometryGetter.java   |  18 +---
 .../sis/internal/sql/feature/InfoStatements.java   | 115 +++++++++++++--------
 .../sis/internal/sql/feature/QueryAnalyzer.java    |   2 +-
 .../org/apache/sis/internal/sql/feature/Table.java |   7 ++
 .../sis/internal/sql/feature/TableAnalyzer.java    |   4 +-
 .../sis/internal/sql/feature/ValueGetter.java      |  16 ---
 .../sis/internal/sql/postgis/ExtendedInfo.java     |  36 +++++--
 .../apache/sis/internal/sql/postgis/Postgres.java  |   3 +
 .../sis/internal/sql/postgis/RasterGetter.java     |  90 ++++++++++++++++
 .../sis/internal/sql/postgis/RasterReader.java     |  38 ++++++-
 .../java/org/apache/sis/storage/sql/SQLStore.java  |   3 +
 .../sis/internal/sql/postgis/PostgresTest.java     |  12 ++-
 .../sis/internal/sql/postgis/RasterReaderTest.java |  13 ++-
 16 files changed, 324 insertions(+), 110 deletions(-)

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 cd28d13..480b10c 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
@@ -110,10 +110,12 @@ public final class Column {
     private GeometryType geometryType;
 
     /**
-     * If this column is a geometry column, the Coordinate Reference System 
(CRS). Otherwise {@code null}.
+     * If this column is a geometry or raster column, the Coordinate Reference 
System (CRS). Otherwise {@code null}.
      * This is determined from the geometry Spatial Reference Identifier 
(SRID).
+     *
+     * @see #getDefaultCRS()
      */
-    private CoordinateReferenceSystem geometryCRS;
+    private CoordinateReferenceSystem defaultCRS;
 
     /**
      * Converter from {@link ResultSet} column value to value stored in the 
feature instance.
@@ -161,24 +163,26 @@ public final class Column {
     }
 
     /**
-     * Modifies this column for declaring it as a geometry column.
+     * Modifies this column for declaring it as a geometry or raster column.
      * This method is invoked during inspection of the {@code 
"GEOMETRY_COLUMNS"} table of a spatial database.
+     * It can also be invoked during the inspection of {@code 
"GEOGRAPHY_COLUMNS"} or {@code "RASTER_COLUMNS"}
+     * tables, which are PostGIS extensions. In the raster case, the geometry 
{@code type} argument shall be null.
      *
      * @param  caller  provider of the locale for error message, if any.
-     * @param  type    the type of values in the column, or {@code null} if 
unknown.
+     * @param  type    the type of values in the column, or {@code null} if 
not geometric.
      * @param  crs     the Coordinate Reference System (CRS), or {@code null} 
if unknown.
      */
-    final void setGeometryInfo(final Localized caller, final GeometryType 
type, final CoordinateReferenceSystem crs)
+    final void makeSpatial(final Localized caller, final GeometryType type, 
final CoordinateReferenceSystem crs)
             throws DataStoreContentException
     {
         final String property;
         if (geometryType != null && !geometryType.equals(type)) {
             property = "geometryType";
-        } else if (geometryCRS != null && !geometryCRS.equals(crs)) {
-            property = "geometryCRS";
+        } else if (defaultCRS != null && !defaultCRS.equals(crs)) {
+            property = "defaultCRS";
         } else {
             geometryType = type;
-            geometryCRS = crs;
+            defaultCRS = crs;
             return;
         }
         throw new 
DataStoreContentException(Errors.getResources(caller.getLocale())
@@ -187,22 +191,24 @@ public final class Column {
 
     /**
      * If this column is a geometry column, returns the type of the geometry 
objects.
-     * Otherwise returns {@code null}.
+     * Otherwise returns {@code null} (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 unknown or not 
applicable.
+     * @return type of geometry objects, or {@code null} if this column is not 
a geometry column.
      */
     public final GeometryType getGeometryType() {
         return geometryType;
     }
 
     /**
-     * If this column is a geometry column, returns the coordinate reference 
system.
+     * 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.
      *
-     * @return CRS of geometries in this column, or {@code null} if unknown or 
not applicable.
+     * @return CRS of geometries or rasters in this column, or {@code null} if 
unknown or not applicable.
      */
-    public final CoordinateReferenceSystem getGeometryCRS() {
-        return geometryCRS;
+    public final CoordinateReferenceSystem getDefaultCRS() {
+        return defaultCRS;
     }
 
     /**
@@ -221,7 +227,7 @@ public final class Column {
         if (isNullable) {
             attribute.setMinimumOccurs(0);
         }
-        valueGetter.getDefaultCRS().ifPresent(attribute::setCRS);
+        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 7da6ed9..6944bd7 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
@@ -155,6 +155,14 @@ public class Database<G> extends Syntax  {
     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;
+
+    /**
      * Catalog and schema of the {@value InfoStatements#GEOMETRY_COLUMNS} and
      * {@value InfoStatements#SPATIAL_REF_SYS} tables, or null or empty string 
if none.
      */
@@ -366,6 +374,7 @@ public class Database<G> extends Syntax  {
         for (final Table table : analyzer.finish()) {
             tablesByNames.add(store, table.featureType.getName(), table);
             hasGeometry |= table.hasGeometry;
+            hasRaster   |= table.hasRaster;
         }
         tables = tableList.toArray(new Table[tableList.size()]);
     }
@@ -480,6 +489,7 @@ 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.
      *
      * @return whether at least one geometry column has been found.
      */
@@ -488,6 +498,16 @@ public class Database<G> extends Syntax  {
     }
 
     /**
+     * Returns {@code true} if this database contains at least one raster 
column.
+     * This information can be used for metadata purpose.
+     *
+     * @return whether at least one raster column has been found.
+     */
+    public final boolean hasRaster() {
+        return hasRaster;
+    }
+
+    /**
      * Returns a function for getting values from a column having the given 
definition.
      * The given definition should include data SQL type and type name.
      * If no match is found, then this method returns {@code null}.
@@ -563,7 +583,7 @@ public class Database<G> extends Syntax  {
     protected final ValueGetter<?> forGeometry(final Column columnDefinition) {
         final GeometryType type = columnDefinition.getGeometryType();
         final Class<? extends G> geometryClass = 
geomLibrary.getGeometryClass(type).asSubclass(geomLibrary.rootClass);
-        return new GeometryGetter<>(geomLibrary, geometryClass, 
columnDefinition.getGeometryCRS(), getBinaryEncoding(columnDefinition));
+        return new GeometryGetter<>(geomLibrary, geometryClass, 
columnDefinition.getDefaultCRS(), getBinaryEncoding(columnDefinition));
     }
 
     /**
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java
index 7afcc4a..6da5908 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureAnalyzer.java
@@ -23,6 +23,7 @@ import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.sql.SQLException;
 import org.opengis.util.GenericName;
+import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
@@ -126,6 +127,13 @@ abstract class FeatureAnalyzer {
     boolean hasGeometry;
 
     /**
+     * Whether this table or view contains at least one raster column.
+     *
+     * @see Database#hasRaster
+     */
+    boolean hasRaster;
+
+    /**
      * Creates a new analyzer.
      *
      * @param  id  the catalog, schema and table name of the table to analyze.
@@ -228,11 +236,12 @@ abstract class FeatureAnalyzer {
              * If geometry columns are found, the first one will be defined as 
the default geometry.
              * Note: a future version may allow user to select which column 
should be the default.
              */
-            if (Geometries.isKnownType(getter.valueType)) {
-                if (!hasGeometry) {
-                    hasGeometry = true;
-                    attribute.addRole(AttributeRole.DEFAULT_GEOMETRY);
-                }
+            if (!hasGeometry && Geometries.isKnownType(getter.valueType)) {
+                hasGeometry = true;
+                attribute.addRole(AttributeRole.DEFAULT_GEOMETRY);
+            }
+            if (!hasRaster) {
+                hasRaster = 
GridCoverage.class.isAssignableFrom(getter.valueType);
             }
         }
         /*
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryGetter.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryGetter.java
index 5d6f2c6..e3622ce 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryGetter.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/GeometryGetter.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.internal.sql.feature;
 
-import java.util.Optional;
 import java.util.OptionalInt;
 import java.nio.ByteBuffer;
 import java.sql.ResultSet;
@@ -52,7 +51,10 @@ import org.apache.sis.internal.feature.Geometries;
  * @param <V> the type of geometry objects returned by this getter.
  *
  * @version 1.2
- * @since   1.1
+ *
+ * @see org.apache.sis.internal.sql.postgis.RasterGetter
+ *
+ * @since 1.1
  * @module
  */
 final class GeometryGetter<G, V extends G> extends ValueGetter<V> {
@@ -74,13 +76,12 @@ final class GeometryGetter<G, V extends G> extends 
ValueGetter<V> {
 
     /**
      * Creates a new reader. The same instance can be reused for parsing an 
arbitrary
-     * amount of geometries sharing the same CRS.
+     * amount of geometries sharing the same default CRS.
      *
      * @param  geometryFactory  the factory to use for creating geometries 
from WKB definitions.
      * @param  geometryClass    the type of geometry to be returned by this 
{@code ValueGetter}.
      * @param  defaultCRS       the CRS to use if none can be mapped from the 
SRID, or {@code null} if none.
      * @param  encoding         the way binary data are encoded in the 
geometry column.
-     * @return a WKB reader resolving SRID with the specified mapper and 
default CRS.
      */
     GeometryGetter(final Geometries<G> geometryFactory, final Class<V> 
geometryClass,
             final CoordinateReferenceSystem defaultCRS, final BinaryEncoding 
encoding)
@@ -92,15 +93,6 @@ final class GeometryGetter<G, V extends G> extends 
ValueGetter<V> {
     }
 
     /**
-     * Returns the default coordinate reference system for this column.
-     * The default CRS is declared in the {@code "GEOMETRY_COLUMNS"} table.
-     */
-    @Override
-    public Optional<CoordinateReferenceSystem> getDefaultCRS() {
-        return Optional.ofNullable(defaultCRS);
-    }
-
-    /**
      * 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.
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 2ec5cf5..9ade14d 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
@@ -89,16 +89,38 @@ public class InfoStatements implements Localized, 
AutoCloseable {
     static final String GEOMETRY_COLUMNS = "GEOMETRY_COLUMNS";
 
     /**
-     * Parameter value for telling that {@code "GEOMETRY_TYPE"} column is 
expected to contain an integer value.
-     * This is the encoding used in OGC standard.
+     * Specifies how the geometry type is encoded in the {@code 
"GEOMETRY_TYPE"} column.
+     * The OGC standard defines numeric values, but PostGIS uses textual 
values.
+     *
+     * @see #configureSpatialColumns(PreparedStatement, TableReference, Map, 
GeometryTypeEncoding)
      */
-    protected static final int COLUMN_TYPE_IS_NUMERIC = 1;
+    protected enum GeometryTypeEncoding {
+        /**
+         * {@code "GEOMETRY_TYPE"} column is expected to contain an integer 
value.
+         * This is the encoding used in OGC standard.
+         */
+        NUMERIC,
 
-    /**
-     * Parameter value for telling that {@code "GEOMETRY_TYPE"} column is 
expected to contain a textual value.
-     * This is the encoding used by PostGIS, but naming the column as {@code 
"TYPE"} for avoiding confusion.
-     */
-    protected static final int COLUMN_TYPE_IS_TEXTUAL = 2;
+        /**
+         * {@code "GEOMETRY_TYPE"} column is expected to contain a textual 
value.
+         * This is the encoding used by PostGIS, but using a different column 
name
+         * ({@code "TYPE"} instead of {@code "GEOMETRY_TYPE"}) for avoiding 
confusion.
+         */
+        TEXTUAL() {
+            @Override GeometryType parse(final ResultSet result, final int 
columnIndex) throws SQLException {
+                return GeometryType.forName(result.getString(columnIndex));
+            }
+        };
+
+        /**
+         * Decodes the geometry type encoded in the specified column of the 
given result set.
+         * If there is no type information, then this method returns {@code 
null}.
+         */
+        GeometryType parse(final ResultSet result, final int columnIndex) 
throws SQLException {
+            final int code = result.getInt(columnIndex);
+            return result.wasNull() ? null : GeometryType.forBinaryType(code);
+        }
+    }
 
     /**
      * The database that created this set of cached statements. This object 
includes the
@@ -173,29 +195,43 @@ public class InfoStatements implements Localized, 
AutoCloseable {
     }
 
     /**
-     * Prepares the statement for fetching information about all geometry 
columns in a specified table.
-     * This method is for {@link #completeGeometryColumns(TableReference, 
Map)} implementations.
+     * Appends a statement after {@code "WHERE"} such as {@code ""F_TABLE_NAME 
= ?"}.
+     *
+     * @param  sql     the builder where to add the SQL statement.
+     * @param  prefix  the column name prefix: {@code 'F'} for features or 
{@code 'R'} for rasters.
+     * @param  column  the column name (e.g. {@code "TABLE_NAME"}.
+     * @return the given SQL builder.
+     */
+    private static SQLBuilder appendCondition(final SQLBuilder sql, final char 
prefix, final String column) {
+        return sql.append(prefix).append('_').append(column).append(" = ?");
+    }
+
+    /**
+     * Prepares the statement for fetching information about all geometry or 
raster columns in a specified table.
+     * This method is for {@link #completeIntrospection(TableReference, Map)} 
implementations.
      *
-     * @param  table   name of the geometry table.  Standard value is {@code 
"GEOMETRY_COLUMNS"}.
-     * @param  column  name of the geometry column. Standard value is {@code 
"F_GEOMETRY_COLUMN"}.
-     * @param  type    name of the type column.     Standard value is {@code 
"GEOMETRY_TYPE"}.
+     * @param  table        name of the geometry table. Standard value is 
{@code "GEOMETRY_COLUMNS"}.
+     * @param  prefix       column name prefix: {@code 'F'} for features or 
{@code 'R'} for rasters.
+     * @param  column       name of the geometry column without prefix. 
Standard value is {@code "GEOMETRY_COLUMN"}.
+     * @param  otherColumn  additional columns or {@code null} if none. 
Standard value is {@code "GEOMETRY_TYPE"}.
      * @return the prepared statement for querying the geometry table.
      * @throws SQLException if the statement can not be created.
      */
-    protected final PreparedStatement prepareGeometryStatement(final String 
table, final String column, final String type)
-            throws SQLException
+    protected final PreparedStatement prepareIntrospectionStatement(final 
String table,
+            final char prefix, final String column, final String otherColumn) 
throws SQLException
     {
         final SQLBuilder sql = new 
SQLBuilder(database).append(SQLBuilder.SELECT)
-                .append(column).append(", ").append(type).append(", SRID ");
+                .append(prefix).append('_').append(column).append(", SRID ");
+        if (otherColumn != null) sql.append(", ").append(otherColumn);
         appendFrom(sql, table);
-        if (database.supportsCatalogs) sql.append("F_TABLE_CATALOG = ? AND ");
-        if (database.supportsSchemas)  sql.append("F_TABLE_SCHEMA = ? AND ");
-        sql.append("F_TABLE_NAME = ?");
+        if (database.supportsCatalogs) appendCondition(sql, prefix, 
"TABLE_CATALOG").append(" AND ");
+        if (database.supportsSchemas)  appendCondition(sql, prefix, 
"TABLE_SCHEMA" ).append(" AND ");
+        appendCondition(sql, prefix, "TABLE_NAME");
         return connection.prepareStatement(sql.toString());
     }
 
     /**
-     * Gets all geometry columns for the given table and sets the geometry 
information on the corresponding columns.
+     * Gets all geometry and raster columns for the given table and sets 
information on the corresponding columns.
      * Column instances in the {@code columns} map are modified in-place (the 
map itself is not modified).
      * This method should be invoked before the {@link Column#valueGetter} 
field is set.
      *
@@ -205,21 +241,23 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * @throws ParseException if the WKT can not be parsed.
      * @throws SQLException if a SQL error occurred.
      */
-    public void completeGeometryColumns(final TableReference source, final 
Map<String,Column> columns) throws Exception {
+    public void completeIntrospection(final TableReference source, final 
Map<String,Column> columns) throws Exception {
         if (geometryColumns == null) {
-            geometryColumns = prepareGeometryStatement(GEOMETRY_COLUMNS, 
"F_GEOMETRY_COLUMN", "GEOMETRY_TYPE");
+            geometryColumns = prepareIntrospectionStatement(GEOMETRY_COLUMNS, 
'F', "GEOMETRY_COLUMN", "GEOMETRY_TYPE");
         }
-        completeGeometryColumns(geometryColumns, source, columns, 
COLUMN_TYPE_IS_NUMERIC);
+        configureSpatialColumns(geometryColumns, source, columns, 
GeometryTypeEncoding.NUMERIC);
     }
 
     /**
-     * Implementation of {@link #completeGeometryColumns(TableReference, 
Map)}, as a separated methods
-     * for allowing sub-classes to override above-cited method.
+     * Implementation of {@link #completeIntrospection(TableReference, Map)} 
for geometries,
+     * as a separated methods for allowing sub-classes to override above-cited 
method.
+     * May also be used for non-geometric columns such as rasters, in which 
case the
+     * {@code typeValueKind} argument shall be {@code null}.
      *
-     * @param  columnQuery    a statement prepared by {@link 
#prepareGeometryStatement(String, String, String)}.
+     * @param  columnQuery    a statement prepared by {@link 
#prepareIntrospectionStatement(String, char, String, String)}.
      * @param  source         the table for which to get all geometry columns.
      * @param  columns        all columns for the specified table. Keys are 
column names.
-     * @param  typeValueKind  {@link #COLUMN_TYPE_IS_NUMERIC}, {@link 
#COLUMN_TYPE_IS_TEXTUAL} or 0 if none.
+     * @param  typeValueKind  {@code NUMERIC}, {@code TEXTUAL} or {@code null} 
if none.
      * @throws DataStoreContentException if a logical error occurred in 
processing data.
      * @throws ParseException if the WKT can not be parsed.
      * @throws SQLException if a SQL error occurred.
@@ -229,8 +267,8 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      *       unless user has statically defined its column to match a specific 
geometry type/SRID.
      *       Source: https://gis.stackexchange.com/a/376947/182809
      */
-    protected final void completeGeometryColumns(final PreparedStatement 
columnQuery, final TableReference source,
-                                       final Map<String,Column> columns, final 
int typeValueKind) throws Exception
+    protected final void configureSpatialColumns(final PreparedStatement 
columnQuery, final TableReference source,
+            final Map<String,Column> columns, final GeometryTypeEncoding 
typeValueKind) throws Exception
     {
         int p = 0;
         if (database.supportsCatalogs) columnQuery.setString(++p, 
source.catalog);
@@ -240,22 +278,15 @@ public class InfoStatements implements Localized, 
AutoCloseable {
             while (result.next()) {
                 final Column target = columns.get(result.getString(1));
                 if (target != null) {
+                    final CoordinateReferenceSystem crs = 
fetchCRS(result.getInt(2));
                     GeometryType type = null;
-                    switch (typeValueKind) {
-                        case COLUMN_TYPE_IS_TEXTUAL: {
-                            type = GeometryType.forName(result.getString(2));
-                            break;
-                        }
-                        case COLUMN_TYPE_IS_NUMERIC: {
-                            final int code = result.getInt(2);
-                            if (!result.wasNull()) {
-                                type = GeometryType.forBinaryType(code);
-                            }
-                            break;
+                    if (typeValueKind != null) {
+                        type = typeValueKind.parse(result, 3);
+                        if (type == null) {
+                            type = GeometryType.GEOMETRY;
                         }
                     }
-                    final CoordinateReferenceSystem crs = 
fetchCRS(result.getInt(3));
-                    target.setGeometryInfo(this, type, crs);
+                    target.makeSpatial(this, type, crs);
                 }
             }
         }
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java
index 164acd4..ec638ea 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/QueryAnalyzer.java
@@ -168,7 +168,7 @@ final class QueryAnalyzer extends FeatureAnalyzer {
         final InfoStatements spatialInformation = analyzer.spatialInformation;
         if (spatialInformation != null) {
             for (final Map.Entry<TableReference, Map<String,Column>> entry : 
columnsPerTable.entrySet()) {
-                spatialInformation.completeGeometryColumns(entry.getKey(), 
entry.getValue());
+                spatialInformation.completeIntrospection(entry.getKey(), 
entry.getValue());
             }
         }
         /*
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 a0f71a5..836d92e 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
@@ -125,6 +125,11 @@ final class Table extends AbstractFeatureSet {
     final boolean hasGeometry;
 
     /**
+     * {@code true} if this table contains at least one raster column.
+     */
+    final boolean hasRaster;
+
+    /**
      * Map from attribute name to columns. This is built from {@link #columns} 
array when first needed.
      *
      * @see #getColumn(String)
@@ -169,6 +174,7 @@ final class Table extends AbstractFeatureSet {
         primaryKey    = analyzer.createAssociations(exportedKeys);   // Must 
be after `spec.createAttributes(…)`.
         featureType   = analyzer.buildFeatureType();
         hasGeometry   = analyzer.hasGeometry;
+        hasRaster     = analyzer.hasRaster;
     }
 
     /**
@@ -191,6 +197,7 @@ final class Table extends AbstractFeatureSet {
         exportedKeys = parent.exportedKeys;
         featureType  = parent.featureType;
         hasGeometry  = parent.hasGeometry;
+        hasRaster    = parent.hasRaster;
     }
 
     /**
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java
index 9f76123..8b2b415 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/TableAnalyzer.java
@@ -153,7 +153,7 @@ final class TableAnalyzer extends FeatureAnalyzer {
     @Override
     final Column[] createAttributes() throws Exception {
         /*
-         * Get all columns in advance because `completeGeometryColumns(…)`
+         * Get all columns in advance because `completeIntrospection(…)`
          * needs to be invoked before to invoke `database.getMapping(column)`.
          */
         final Map<String,Column> columns = new LinkedHashMap<>();
@@ -167,7 +167,7 @@ final class TableAnalyzer extends FeatureAnalyzer {
         }
         final InfoStatements spatialInformation = analyzer.spatialInformation;
         if (spatialInformation != null) {
-            spatialInformation.completeGeometryColumns(id, columns);
+            spatialInformation.completeIntrospection(id, columns);
         }
         /*
          * Analyze the type of each column, which may be geometric as a 
consequence of above call.
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 ad0adfc..99ae321 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
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.internal.sql.feature;
 
-import java.util.Optional;
 import java.util.Calendar;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -29,7 +28,6 @@ import java.time.OffsetDateTime;
 import java.time.OffsetTime;
 import java.time.ZoneOffset;
 import java.math.BigDecimal;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.util.ArgumentChecks;
 
 
@@ -90,20 +88,6 @@ public abstract class ValueGetter<T> {
     public abstract T getValue(InfoStatements stmts, ResultSet source, int 
columnIndex) throws Exception;
 
     /**
-     * Returns the default coordinate reference system for this column.
-     * The default CRS is declared in the {@code "GEOMETRY_COLUMNS"} table.
-     *
-     * <div class="note"><b>Note:</b>
-     * this method could be used not only for geometric fields, but also on 
numeric ones representing 1D systems.
-     * </div>
-     *
-     * @return the default coordinate reference system for values in this 
column.
-     */
-    public Optional<CoordinateReferenceSystem> getDefaultCRS() {
-        return Optional.empty();
-    }
-
-    /**
      * A getter of {@link Object} values from the current row of a {@link 
ResultSet}.
      * This getter delegates to {@link ResultSet#getObject(int)} and returns 
that value with no change.
      */
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtendedInfo.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtendedInfo.java
index 398d7d2..e1bbe63d 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtendedInfo.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/ExtendedInfo.java
@@ -31,7 +31,7 @@ import org.apache.sis.internal.sql.feature.InfoStatements;
  *
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @since   1.1
+ * @since   1.2
  * @version 1.1
  * @module
  */
@@ -43,6 +43,16 @@ final class ExtendedInfo extends InfoStatements {
     private PreparedStatement geographyColumns;
 
     /**
+     * A statement for fetching raster information for a specific column.
+     */
+    protected PreparedStatement rasterColumns;
+
+    /**
+     * The object for reading a raster, or {@code null} if not yet created.
+     */
+    private RasterReader rasterReader;
+
+    /**
      * Creates an initially empty {@code PostgisStatements} which will use
      * the given connection for creating {@link PreparedStatement}s.
      */
@@ -57,15 +67,29 @@ final class ExtendedInfo extends InfoStatements {
      * @param  columns  all columns for the specified table. Keys are column 
names.
      */
     @Override
-    public void completeGeometryColumns(final TableReference source, final 
Map<String,Column> columns) throws Exception {
+    public void completeIntrospection(final TableReference source, final 
Map<String,Column> columns) throws Exception {
         if (geometryColumns == null) {
-            geometryColumns = prepareGeometryStatement("geometry_columns", 
"f_geometry_column", "type");
+            geometryColumns = 
prepareIntrospectionStatement("geometry_columns", 'f', "geometry_column", 
"type");
         }
         if (geographyColumns == null) {
-            geographyColumns = prepareGeometryStatement("geography_columns", 
"f_geography_column", "type");
+            geographyColumns = 
prepareIntrospectionStatement("geography_columns", 'f', "geography_column", 
"type");
+        }
+        if (rasterColumns == null) {
+            rasterColumns = prepareIntrospectionStatement("raster_columns", 
'r', "raster_column", null);
+        }
+        configureSpatialColumns(geometryColumns,  source, columns, 
GeometryTypeEncoding.TEXTUAL);
+        configureSpatialColumns(geographyColumns, source, columns, 
GeometryTypeEncoding.TEXTUAL);
+        configureSpatialColumns(rasterColumns,    source, columns, null);
+    }
+
+    /**
+     * Returns a reader for decoding PostGIS Raster binary format to grid 
coverage instances.
+     */
+    final RasterReader getRasterReader() {
+        if (rasterReader == null) {
+            rasterReader = new RasterReader(this);
         }
-        completeGeometryColumns(geometryColumns,  source, columns, 
COLUMN_TYPE_IS_TEXTUAL);
-        completeGeometryColumns(geographyColumns, source, columns, 
COLUMN_TYPE_IS_TEXTUAL);
+        return rasterReader;
     }
 
     /**
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 86f7f77..87122c2 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
@@ -116,6 +116,9 @@ public final class Postgres<G> extends Database<G> {
         if ("geography".equalsIgnoreCase(columnDefinition.typeName)) {
             return forGeometry(columnDefinition);
         }
+        if ("raster".equalsIgnoreCase(columnDefinition.typeName)) {
+            return new RasterGetter(columnDefinition.getDefaultCRS(), 
getBinaryEncoding(columnDefinition));
+        }
         return super.getMapping(columnDefinition);
     }
 
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterGetter.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterGetter.java
new file mode 100644
index 0000000..3912209
--- /dev/null
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterGetter.java
@@ -0,0 +1,90 @@
+/*
+ * 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.ResultSet;
+import java.io.InputStream;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.internal.sql.feature.BinaryEncoding;
+import org.apache.sis.internal.sql.feature.InfoStatements;
+import org.apache.sis.internal.sql.feature.ValueGetter;
+import org.apache.sis.coverage.grid.GridCoverage;
+
+
+/**
+ * Reader of rasters encoded in Well Known Binary (WKB) format.
+ * At the time of writing this class, raster WKB is a PostGIS-specific format.
+ *
+ * <h2>Multi-threading</h2>
+ * {@code RasterGetter} instances shall be thread-safe.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ *
+ * @see org.apache.sis.internal.sql.feature.GeometryGetter
+ *
+ * @since 1.2
+ * @module
+ */
+final class RasterGetter extends ValueGetter<GridCoverage> {
+    /**
+     * The Coordinate Reference System if {@link InfoStatements} can not map 
the SRID.
+     * This is {@code null} if there is no default.
+     */
+    private final CoordinateReferenceSystem defaultCRS;
+
+    /**
+     * The way binary data are encoded in the raster column.
+     */
+    private final BinaryEncoding encoding;
+
+    /**
+     * Creates a new reader. The same instance can be reused for parsing an 
arbitrary
+     * amount of rasters sharing the same default CRS.
+     *
+     * @param  defaultCRS  the CRS to use if none can be mapped from the SRID, 
or {@code null} if none.
+     * @param  encoding    the way binary data are encoded in the raster 
column.
+     */
+    RasterGetter(final CoordinateReferenceSystem defaultCRS, final 
BinaryEncoding encoding) {
+        super(GridCoverage.class);
+        this.defaultCRS = defaultCRS;
+        this.encoding   = encoding;
+    }
+
+    /**
+     * 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 raster 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 GridCoverage getValue(final InfoStatements stmts, final ResultSet 
source, final int columnIndex) throws Exception {
+        InputStream stream = source.getBinaryStream(columnIndex);
+        if (stream != null && stmts instanceof ExtendedInfo) {
+            stream = encoding.decode(stream);
+            final RasterReader reader = ((ExtendedInfo) 
stmts).getRasterReader();
+            reader.defaultCRS = defaultCRS;
+            return reader.readAsCoverage(reader.channel(stream));
+        }
+        return null;
+    }
+}
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java
index 693c6ed..82061cd 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java
@@ -19,6 +19,8 @@ package org.apache.sis.internal.sql.postgis;
 import java.util.List;
 import java.util.Arrays;
 import java.nio.ByteOrder;
+import java.nio.ByteBuffer;
+import java.io.InputStream;
 import java.io.IOException;
 import java.lang.reflect.Array;
 import java.awt.image.ColorModel;
@@ -35,6 +37,7 @@ import java.awt.image.DataBufferDouble;
 import java.awt.image.WritableRaster;
 import java.awt.image.BufferedImage;
 import java.awt.image.RasterFormatException;
+import java.nio.channels.Channels;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridCoverage;
@@ -81,6 +84,12 @@ public final class RasterReader extends RasterFormat {
     private AffineTransform2D gridToCRS;
 
     /**
+     * The default Coordinate Reference System (CRS) if the raster does not 
specify a CRS.
+     * This is {@code null} if there is no default.
+     */
+    public CoordinateReferenceSystem defaultCRS;
+
+    /**
      * The spatial reference identifier, or 0 if undefined.
      * Note that this is a primary key in the {@code "spatial_ref_sys"} table, 
not necessarily an EPSG code.
      */
@@ -98,6 +107,12 @@ public final class RasterReader extends RasterFormat {
     private transient SampleModel cachedModel;
 
     /**
+     * A temporary buffer for the bytes in the process of being decoded.
+     * Initially null and created when first needed.
+     */
+    private ByteBuffer buffer;
+
+    /**
      * Creates a new reader. If the {@code spatialRefSys} argument is non-null,
      * then this object is valid only as long as the caller holds a connection 
to the database.
      *
@@ -342,14 +357,15 @@ public final class RasterReader extends RasterFormat {
         if (image == null) {
             return null;
         }
-        final CoordinateReferenceSystem crs;
+        CoordinateReferenceSystem crs = null;
         final int srid = getSRID();
         if (spatialRefSys != null) {
             crs = spatialRefSys.fetchCRS(srid);
         } else if (srid > 0) {
             crs = CRS.forCode(Constants.EPSG + ':' + srid);
-        } else {
-            crs = null;
+        }
+        if (crs == null) {
+            crs = defaultCRS;
         }
         final GridExtent   extent = new GridExtent(image.getWidth(), 
image.getHeight());
         final GridGeometry domain = new GridGeometry(extent, ANCHOR, 
getGridToCRS(), crs);
@@ -375,4 +391,20 @@ public final class RasterReader extends RasterFormat {
         }
         return new GridCoverage2D(domain, range, image);
     }
+
+    /**
+     * Wraps the given input stream into a channel that can be used by {@code 
read(…)} methods in this class.
+     * The returned channel should be used and discarded before to create a 
new {@code ChannelDataInput},
+     * because this method recycles the same {@link ByteBuffer}.
+     *
+     * @param  input  the input stream to wrap.
+     * @return a channel together with a buffer.
+     * @throws IOException if an error occurred while reading data from the 
input stream.
+     */
+    public ChannelDataInput channel(final InputStream input) throws 
IOException {
+        if (buffer == null) {
+            buffer = ByteBuffer.allocate(8192);
+        }
+        return new ChannelDataInput("raster", Channels.newChannel(input), 
buffer, false);
+    }
 }
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java
index 5a0f9be..7d2dd94 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java
@@ -248,6 +248,9 @@ public class SQLStore extends DataStore implements 
Aggregate {
                 if (model.hasGeometry()) {
                     
builder.addSpatialRepresentation(SpatialRepresentationType.VECTOR);
                 }
+                if (model.hasRaster()) {
+                    
builder.addSpatialRepresentation(SpatialRepresentationType.GRID);
+                }
                 model.listTables(c.getMetaData(), builder);
             } catch (DataStoreException e) {
                 throw e;
diff --git 
a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/PostgresTest.java
 
b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/PostgresTest.java
index 7b67836..63af275 100644
--- 
a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/PostgresTest.java
+++ 
b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/PostgresTest.java
@@ -33,11 +33,13 @@ import org.apache.sis.storage.sql.SQLStoreProvider;
 import org.apache.sis.storage.sql.ResourceDefinition;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.sql.SQLStoreTest;
+import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.sql.feature.BinaryEncoding;
 import org.apache.sis.internal.sql.feature.GeometryGetterTest;
 import org.apache.sis.internal.feature.jts.JTS;
 import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 import org.apache.sis.test.sql.TestDatabase;
 import org.apache.sis.test.DependsOn;
@@ -163,12 +165,15 @@ public final strictfp class PostgresTest extends TestCase 
{
      * This method performs only a superficial verification of geometries.
      */
     private static void validate(final Feature feature) {
-        final String   filename = 
feature.getPropertyValue("filename").toString();
-        final Geometry geometry = (Geometry) 
feature.getPropertyValue("geometry");
-        final int      geomSRID;
+        final String       filename = 
feature.getPropertyValue("filename").toString();
+        final Geometry     geometry = (Geometry) 
feature.getPropertyValue("geometry");
+        final GridCoverage raster   = (GridCoverage) 
feature.getPropertyValue("image");
+        final int geomSRID;
         switch (filename) {
             case "raster-ushort.wkb": {
                 assertNull(geometry);
+                RasterReaderTest.compareReadResult(TestRaster.USHORT, raster);
+                assertSame(CommonCRS.WGS84.normalizedGeographic(), 
raster.getCoordinateReferenceSystem());
                 return;
             }
             case "point-prj": {
@@ -190,5 +195,6 @@ public final strictfp class PostgresTest extends TestCase {
         } catch (FactoryException e) {
             throw new AssertionError(e);
         }
+        assertNull(raster);
     }
 }
diff --git 
a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/RasterReaderTest.java
 
b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/RasterReaderTest.java
index fc7595a..619e526 100644
--- 
a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/RasterReaderTest.java
+++ 
b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/postgis/RasterReaderTest.java
@@ -46,14 +46,14 @@ public final strictfp class RasterReaderTest extends 
TestCase {
      */
     @Test
     public void testUShort() throws Exception {
-        compareReadResult(TestRaster.USHORT);
+        RasterReaderTest.compareReadResult(TestRaster.USHORT);
     }
 
     /**
      * Reads the file for the given test enumeration and compares with the 
expected raster.
      */
     private static void compareReadResult(final TestRaster test) throws 
Exception {
-        compareReadResult(test, new RasterReader(null), test.input());
+        RasterReaderTest.compareReadResult(test, new RasterReader(null), 
test.input());
     }
 
     /**
@@ -63,9 +63,16 @@ public final strictfp class RasterReaderTest extends 
TestCase {
     static void compareReadResult(final TestRaster test, final RasterReader 
reader, final ChannelDataInput input) throws Exception {
         final GridCoverage coverage = reader.readAsCoverage(input);
         input.channel.close();
-        final RenderedImage image = coverage.render(null);
         assertEquals(TestRaster.SRID, reader.getSRID());
         assertEquals(TestRaster.getGridToCRS(), reader.getGridToCRS());
+        compareReadResult(test, coverage);
+    }
+
+    /**
+     * Compares the given image with the expected raster.
+     */
+    static void compareReadResult(final TestRaster test, final GridCoverage 
coverage) {
+        final RenderedImage image = coverage.render(null);
         final DataBufferUShort expected = (DataBufferUShort) 
test.createRaster().getDataBuffer();
         final DataBufferUShort actual   = (DataBufferUShort) image.getTile(0, 
0).getDataBuffer();
         assertTrue(Arrays.deepEquals(expected.getBankData(), 
actual.getBankData()));

Reply via email to