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 bce9be08df Add a fallback for trying to guess the SRID when the table 
of a column is unknown. This is a workaround for incomplete JDBC drivers (in 
this case, DuckDB 1.2.2.0).
bce9be08df is described below

commit bce9be08dfb17ed33c571e37374dbdecfb1ec0ee
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Apr 22 16:08:25 2025 +0200

    Add a fallback for trying to guess the SRID when the table of a column is 
unknown.
    This is a workaround for incomplete JDBC drivers (in this case, DuckDB 
1.2.2.0).
---
 .../org/apache/sis/metadata/sql/privy/Dialect.java |   2 +
 .../apache/sis/metadata/sql/privy/Supports.java    |   3 +
 .../org/apache/sis/storage/sql/feature/Column.java |  15 ++-
 .../apache/sis/storage/sql/feature/Database.java   |   2 +-
 .../storage/sql/feature/GeometryTypeEncoding.java  |   2 +
 .../sis/storage/sql/feature/InfoStatements.java    | 118 ++++++++++++++++++---
 .../sis/storage/sql/feature/QueryAnalyzer.java     |   2 +-
 .../sis/storage/sql/feature/SpatialSchema.java     |   9 +-
 .../sis/storage/sql/feature/TableAnalyzer.java     |   4 +-
 9 files changed, 134 insertions(+), 23 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java
index 83af6d9816..ddec3b61ab 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java
@@ -18,6 +18,7 @@ package org.apache.sis.metadata.sql.privy;
 
 import java.sql.SQLException;
 import java.sql.DatabaseMetaData;
+import org.apache.sis.util.Workaround;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.privy.Constants;
 
@@ -197,6 +198,7 @@ public enum Dialect {
      * This flag should be {@code false} when the JDBC driver returns a 
non-null catalog name
      * (for example, the database name) but doesn't accept the use of that 
catalog in SQL.
      */
+    @Workaround(library = "DuckDB", version = "1.2.2.0")
     public final boolean supportsCatalog() {
         return (flags & Supports.CATALOG) != 0;
     }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
index b4f2086e1b..0820e1ea8f 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.metadata.sql.privy;
 
+import org.apache.sis.util.Workaround;
+
 
 /**
  * Enumeration of features that may be supported by a database.
@@ -71,6 +73,7 @@ final class Supports {
      * This flag should be {@code false} when the JDBC driver returns a 
non-null catalog name
      * (for example, the database name) but doesn't accept the use of that 
catalog in SQL.
      */
+    @Workaround(library = "DuckDB", version = "1.2.2.0")
     static final int CATALOG = 64;
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
index 1d23b834c0..9b6dc9444c 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Column.java
@@ -22,6 +22,7 @@ import java.sql.ResultSetMetaData;
 import java.sql.DatabaseMetaData;
 import java.sql.SQLException;
 import java.sql.SQLDataException;
+import java.util.Map;
 import java.util.Optional;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.metadata.sql.privy.Reflection;
@@ -250,14 +251,20 @@ public final class Column implements Cloneable {
      * Tries to parses the geometry type from the field type.
      * This is used as a fallback when no geometry column is found or can be 
used.
      *
-     * @param  database  the database for which to analyze the tables.
+     * @param  analyzer  the object used for analyzing the database schema.
+     * @throws Exception if an error occurred while fetching the 
<abbr>CRS</abbr>.
      */
-    final void tryMakeSpatial(final Database<?> database) {
+    final void tryMakeSpatial(final Analyzer analyzer) throws Exception {
         try {
             geometryType = GeometryType.forName(typeName);
-            geometryAsText = (database.getGeometryEncoding(this) == 
GeometryEncoding.WKT);
+            geometryAsText = (analyzer.database.getGeometryEncoding(this) == 
GeometryEncoding.WKT);
+            final InfoStatements spatialInformation = 
analyzer.spatialInformation;
+            if (spatialInformation != null) {
+                spatialInformation.completeIntrospection(analyzer, null, 
Map.of());
+                defaultCRS = spatialInformation.guessCRS(name);
+            }
         } catch (IllegalArgumentException e) {
-            // Ignore.
+            // The `typeName` value is not the name of a geometry type. Ignore.
         }
     }
 
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 515e6a0737..2fd5f41a7b 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
@@ -319,7 +319,7 @@ public class Database<G> extends Syntax  {
         this.cacheOfCRS    = new Cache<>(7, 2, false);
         this.cacheOfSRID   = new WeakHashMap<>();
         this.tablesByNames = new FeatureNaming<>();
-        supportsCatalogs   = metadata.supportsCatalogsInDataManipulation();
+        supportsCatalogs   = dialect.supportsCatalog() && 
metadata.supportsCatalogsInDataManipulation();
         supportsSchemas    = metadata.supportsSchemasInDataManipulation();
         supportsJavaTime   = dialect.supportsJavaTime();
         crsEncodings       = EnumSet.noneOf(CRSEncoding.class);
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryTypeEncoding.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryTypeEncoding.java
index 6b8c7859f2..ef4d28dd6e 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryTypeEncoding.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryTypeEncoding.java
@@ -48,6 +48,8 @@ public enum GeometryTypeEncoding {
     /**
      * 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}.
+     *
+     * @throws IllegalArgumentException if the type cannot be decoded.
      */
     GeometryType parse(final ResultSet result, final int columnIndex) throws 
SQLException {
         final int code = result.getInt(columnIndex);
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 294cd19b51..f2b5275f52 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
@@ -57,6 +57,7 @@ import org.apache.sis.util.privy.Constants;
 import org.apache.sis.io.wkt.Convention;
 import org.apache.sis.io.wkt.WKTFormat;
 import org.apache.sis.io.wkt.Warnings;
+import org.apache.sis.util.Workaround;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.metadata.Identifier;
@@ -115,35 +116,69 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * A statement for fetching geometric information for a specific column.
      * May be {@code null} if not yet prepared or if the table does not exist.
      * This field is valid if {@link #isAnalysisPrepared} is {@code true}.
+     *
+     * @see #isAnalysisPrepared
+     * @see #completeIntrospection(Analyzer, TableReference, Map)
      */
     protected PreparedStatement geometryColumns;
 
     /**
      * Whether the statements for schema analysis have been prepared.
-     * Includes {@link #geometryColumns}, but not fetching the CRS.
-     * A statement may still be null if the table has not been found.
+     * This flag tells whether the statements for the following information 
are valid even if {@code null}:
+     *
+     * <ul>
+     *   <li>{@linkplain #geometryColumns Geometry columns}</li>
+     *   <li>Geography columns (specific to PostGIS)</li>
+     *   <li>Raster columns (specific to PostGIS)</li>
+     * </ul>
+     *
+     * This flag does not apply to the statements working on the {@code 
SPATIAL_REF_SYS} table,
+     * which is assumed to always exist.
      */
     protected boolean isAnalysisPrepared;
 
     /**
-     * The statement for fetching CRS Well-Known Text (WKT) from a SRID code.
+     * The statement for fetching a SRID from a geometry column table when the 
column's table is unknown.
+     * This is a workaround for <abbr>JDBC</abbr> drivers that do not provide 
this information.
+     * It is created only if requested.
      *
-     * @see <a 
href="http://postgis.refractions.net/documentation/manual-1.3/ch04.html#id2571265";>PostGIS
 documentation</a>
+     * @see #guessCRS(String)
      */
-    private PreparedStatement wktFromSrid;
+    @Workaround(library = "DuckDB", version = "1.2.2.0")
+    private PreparedStatement sridForUnknownTable;
 
     /**
      * The statement for fetching a SRID from a CRS and its set of authority 
codes.
+     * Created when first needed.
+     *
+     * @see #findOrAddCRS(CoordinateReferenceSystem)
      */
     private PreparedStatement sridFromCRS;
 
     /**
-     * The object to use for parsing or formatting Well-Known Text (WKT), 
created when first needed.
+     * The statement for fetching CRS Well-Known Text (<abbr>WKT</abbr>) from 
a <abbr>SRID</abbr> code.
+     * Created when first needed.
+     *
+     * @see #parseCRS(int)
+     * @see <a 
href="http://postgis.refractions.net/documentation/manual-1.3/ch04.html#id2571265";>PostGIS
 documentation</a>
+     */
+    private PreparedStatement wktFromSrid;
+
+    /**
+     * The object to use for parsing or formatting Well-Known Text 
(<abbr>WKT</abbr>).
+     * Created when first needed.
      *
      * @see #wktFormat()
      */
     private WKTFormat wktFormat;
 
+    /**
+     * Whether an error occurred while reading the geometry type.
+     * In such case, the type default to {@link GeometryType#GEOMETRY}.
+     * This flag is used for reporting the warning only once.
+     */
+    private boolean cannotReadGeometryType;
+
     /**
      * Creates an initially empty {@code CachedStatements} which will use
      * the given connection for creating {@link PreparedStatement}s.
@@ -194,6 +229,11 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * Prepares the statement for fetching information about all geometry or 
raster columns in a specified table.
      * This method is for {@link #completeIntrospection(Analyzer, 
TableReference, Map)} implementations.
      *
+     * <h4>PostGIS special case</h4>
+     * By default, the {@code geomColNameColumn} and {@code geomTypeColumn} 
argument values are fetched from the
+     * {@link SpatialSchema}. However, PostGIS uses a non-standard {@code 
geomTypeColumn} value. It also has many
+     * "geometry columns"-like tables. This is handled by overriding {@code 
completeIntrospection(…)}.
+     *
      * @param  analyzer           the opaque temporary object used for 
analyzing the database schema.
      * @param  table              name of the geometry table. Standard value 
is {@code "GEOMETRY_COLUMNS"}.
      * @param  raster             whether the statement is for raster table 
instead of geometry table.
@@ -214,7 +254,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
         if (geomColNameColumn == null) {
             geomColNameColumn = schema.geomColNameColumn;
         }
-        appendColumn(sql, raster, geomColNameColumn).append(", 
").append(schema.crsIdentifierColumn).append(' ');
+        appendColumn(sql, raster, geomColNameColumn).append(", 
").append(schema.crsIdentifierColumn);
         if (geomTypeColumn == null) {
             geomTypeColumn = schema.geomTypeColumn;
         }
@@ -259,8 +299,11 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * This method should be invoked at least once before the {@link 
Column#valueGetter} field is set.
      * It is invoked again for each table or query to analyze.
      *
+     * <p>This method may be invoked with a null {@code source} and empty 
{@code columns}
+     * for ensuring that {@link #geometryColumns} is initialized but without 
executing it.</p>
+     *
      * @param  analyzer  the opaque temporary object used for analyzing the 
database schema.
-     * @param  source    the table for which to get all geometry columns.
+     * @param  source    the table for which to get all geometry columns. May 
be null if {@code columns} is empty.
      * @param  columns   all columns for the specified table. Keys are column 
names.
      * @throws DataStoreContentException if a logical error occurred in 
processing data.
      * @throws ParseException if the WKT cannot be parsed.
@@ -273,6 +316,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
         if (!isAnalysisPrepared) {
             isAnalysisPrepared = true;
             geometryColumns = prepareIntrospectionStatement(analyzer, 
schema.geometryColumns, false, null, null);
+            // The `geometryColumns` field may still be null.
         }
         configureSpatialColumns(geometryColumns, source, columns, 
schema.typeEncoding);
     }
@@ -286,7 +330,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
      * value returned by {@link #prepareIntrospectionStatement(Analyzer, 
String, boolean, String, String)}.
      *
      * @param  columnQuery    the statement for fetching information, or 
{@code null} if none.
-     * @param  source         the table for which to get all geometry columns.
+     * @param  source         the table for which to get all geometry columns. 
May be null if {@code columns} is empty.
      * @param  columns        all columns for the specified table. Keys are 
column names.
      * @param  typeValueKind  {@code NUMERIC}, {@code TEXTUAL} or {@code null} 
if none.
      * @throws DataStoreContentException if a logical error occurred in 
processing data.
@@ -301,7 +345,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
     protected final void configureSpatialColumns(final PreparedStatement 
columnQuery, final TableReference source,
             final Map<String,Column> columns, final GeometryTypeEncoding 
typeValueKind) throws Exception
     {
-        if (columnQuery == null) {
+        if (columnQuery == null || columns.isEmpty()) {
             return;
         }
         int p = 0;
@@ -312,18 +356,66 @@ 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;
                     if (typeValueKind != null) {
-                        type = typeValueKind.parse(result, 3);
+                        try {
+                            type = typeValueKind.parse(result, 3);
+                        } catch (IllegalArgumentException e) {
+                            if (!cannotReadGeometryType) {
+                                cannotReadGeometryType = true;
+                                
database.warning(Resources.Keys.CanNotAnalyzeFully, e);
+                            }
+                        }
                         if (type == null) {
                             type = GeometryType.GEOMETRY;
                         }
                     }
-                    target.makeSpatial(database, type, crs);
+                    target.makeSpatial(database, type, 
fetchCRS(result.getInt(2)));
+                }
+            }
+        }
+    }
+
+    /**
+     * Tries to guess the <abbr>CRS</abbr> for the specified column in an 
unknown table.
+     * This is used for queries when the <abbr>JDBC</abbr> driver is 
incomplete.
+     *
+     * @param  column  name of the column in unknown table.
+     * @return the <abbr>CRS</abbr>, or {@code null} if none or ambiguous.
+     * @throws Exception if an error occurred while fetching the 
<abbr>CRS</abbr>.
+     */
+    @Workaround(library = "DuckDB", version = "1.2.2.0")
+    final CoordinateReferenceSystem guessCRS(final String column) throws 
Exception {
+        if (sridForUnknownTable == null) {
+            if (geometryColumns == null) {
+                return null;
+            }
+            final SpatialSchema schema = 
database.getSpatialSchema().orElseThrow();
+            final var sql = new SQLBuilder(database)
+                    .append(SQLBuilder.SELECT).append("DISTINCT ")
+                    .appendIdentifier(schema.crsIdentifierColumn)
+                    .append(" FROM ").appendIdentifier(schema.geometryColumns)
+                    .append(" WHERE 
").appendIdentifier(schema.geomColNameColumn).append("=?");
+            sridForUnknownTable = connection.prepareStatement(sql.toString());
+        }
+        sridForUnknownTable.setString(1, column);
+        CoordinateReferenceSystem first = null;
+        try (ResultSet result = sridForUnknownTable.executeQuery()) {
+            while (result.next()) {
+                final int srid = result.getInt(1);
+                if (!result.wasNull()) {
+                    CoordinateReferenceSystem crs = fetchCRS(srid);
+                    if (crs != null) {
+                        if (first == null) {
+                            first = crs;
+                        } else if (!Utilities.equalsIgnoreMetadata(first, 
crs)) {
+                            return null;
+                        }
+                    }
                 }
             }
         }
+        return first;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java
index b474ece77a..6ebbd52955 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/QueryAnalyzer.java
@@ -171,7 +171,7 @@ final class QueryAnalyzer extends FeatureAnalyzer {
         final var attributes = new ArrayList<Column>();
         for (final Column column : columns) {
             if (fallback) {
-                column.tryMakeSpatial(analyzer.database);
+                column.tryMakeSpatial(analyzer);
             }
             if (createAttribute(column)) {
                 attributes.add(column);
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java
index d4f2b34b9a..c187f0b4d8 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java
@@ -59,8 +59,8 @@ public enum SpatialSchema {
             GeometryTypeEncoding.TEXTUAL),          // How geometry types are 
encoded in the above-cited type column.
 
     /**
-     * Table and column names as specified by ISO-13249 SQL/MM. This is the 
same thing as {@link #SIMPLE_FEATURE}
-     * with only different names. The table definition for CRS is:
+     * Table and column names as specified by ISO-13249 SQL/MM. This is 
similar to {@link #SIMPLE_FEATURE}
+     * with different names and no {@code GEOMETRY_TYPE} column. The table 
definition for CRS is:
      *
      * {@snippet lang="sql" :
      * CREATE TABLE ST_SPATIAL_REFERENCE_SYSTEMS(
@@ -104,6 +104,11 @@ public enum SpatialSchema {
      *   AUTH_SRID INTEGER,
      *   SRTEXT CHARACTER VARYING(2048))
      * }
+     *
+     * <h4>PostGIS special case</h4>
+     * PostGIS uses these table and column names (in lower cases), except the 
{@code GEOMETRY_TYPE} column
+     * which is named only {@code TYPE} in PostGIS. There is no enumeration 
value for PostGIS special case.
+     * Instead, it is handled by {@code 
InfoStatements.completeIntrospection(…)} method overriding.
      */
     SIMPLE_FEATURE(
             "ISO 19125 / OGC Simple feature",       // Human-readable name of 
this spaial schema.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java
index 291a1797b5..28d53c2180 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableAnalyzer.java
@@ -187,9 +187,9 @@ final class TableAnalyzer extends FeatureAnalyzer {
          */
         final var attributes = new ArrayList<Column>();
         for (final Column column : columns.values()) {
-            if (spatialInformation == null) {
+            if (spatialInformation == null || 
spatialInformation.geometryColumns == null) {
                 // Fallback for databases without "geometry columns" table.
-                column.tryMakeSpatial(analyzer.database);
+                column.tryMakeSpatial(analyzer);
             }
             if (createAttribute(column)) {
                 attributes.add(column);

Reply via email to