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 1693635df43fdf95b30e8fe4e449c38018cdffb4
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Mar 18 18:34:51 2025 +0100

    Make the search for primary keys, foreigner keys and row counts
    more robust to databases that do not support those features.
---
 .../apache/sis/storage/sql/feature/Analyzer.java   | 81 ++++++++++++++++------
 .../apache/sis/storage/sql/feature/Database.java   |  7 ++
 .../sis/storage/sql/feature/FeatureAnalyzer.java   |  2 +-
 .../sis/storage/sql/feature/FeatureIterator.java   |  4 ++
 .../sis/storage/sql/feature/QueryAnalyzer.java     |  2 +-
 .../apache/sis/storage/sql/feature/Resources.java  |  5 ++
 .../sis/storage/sql/feature/Resources.properties   |  1 +
 .../storage/sql/feature/Resources_fr.properties    |  3 +-
 .../org/apache/sis/storage/sql/feature/Table.java  |  6 ++
 .../sis/storage/sql/feature/TableAnalyzer.java     | 11 +++
 10 files changed, 99 insertions(+), 23 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java
index a8a6e0fd4d..2f42b0d4bf 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java
@@ -27,10 +27,12 @@ import java.util.HashMap;
 import java.util.Objects;
 import java.util.Locale;
 import java.util.logging.Level;
+import java.util.logging.LogRecord;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.sql.SQLException;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
+import java.sql.SQLFeatureNotSupportedException;
 import javax.sql.DataSource;
 import org.opengis.util.NameFactory;
 import org.opengis.util.NameSpace;
@@ -124,10 +126,10 @@ public final class Analyzer {
      * The last catalog and schema used for creating {@link #namespace}.
      * Used for determining if {@link #namespace} is still valid.
      */
-    private transient String catalog, schema;
+    private transient String lastCatalog, lastSchema;
 
     /**
-     * The namespace created with {@link #catalog} and {@link #schema}.
+     * The namespace created with {@link #lastCatalog} and {@link #lastSchema}.
      *
      * @see #namespace(String, String)
      */
@@ -138,6 +140,11 @@ public final class Analyzer {
      */
     SchemaModifier customizer;
 
+    /**
+     * Exception thrown when some metadata are not available, but the analysis 
can nevertheless continue.
+     */
+    private SQLFeatureNotSupportedException featureNotSupported;
+
     /**
      * Creates a new analyzer for a spatial database.
      * Callers shall invoke {@link #analyze analyze(…)} after this method.
@@ -257,7 +264,21 @@ public final class Analyzer {
     }
 
     /**
-     * Reads a string from the given result set and return a unique instance 
of that string.
+     * Initializes the value getter of the given column. This method shall be 
invoked only after the geometry
+     * columns have been identified. This is invoked (indirectly) during 
{@link Table} construction.
+     */
+    final ValueGetter<?> setValueGetterOf(final Column column) {
+        ValueGetter<?> getter = database.getMapping(column);
+        if (getter == null) {
+            getter = database.getDefaultMapping();
+            warning(Resources.Keys.UnknownType_1, column.typeName);
+        }
+        column.valueGetter = getter;
+        return getter;
+    }
+
+    /**
+     * Reads a string from the given result set and returns a unique instance 
of that string.
      * This method should be invoked only for {@code String} instances that 
are going to be
      * stored in {@link Table} or {@link Relation} structures. There is no 
point to invoke
      * this method for example before to parse the string as a Boolean.
@@ -280,7 +301,7 @@ public final class Analyzer {
      * The namespace sets the name separator to {@code '.'} instead of {@code 
':'}.
      */
     final NameSpace namespace(final String catalog, final String schema) {
-        if (!Objects.equals(this.schema, schema) || 
!Objects.equals(this.catalog, catalog)) {
+        if (!(Objects.equals(lastSchema, schema) && 
Objects.equals(lastCatalog, catalog))) {
             if (schema != null) {
                 final GenericName name;
                 if (catalog == null) {
@@ -292,8 +313,8 @@ public final class Analyzer {
             } else {
                 namespace = null;
             }
-            this.catalog = catalog;
-            this.schema  = schema;
+            lastCatalog = catalog;
+            lastSchema  = schema;
         }
         return namespace;
     }
@@ -356,8 +377,37 @@ public final class Analyzer {
         return resources().getString(Resources.Keys.InternalError);
     }
 
+    /**
+     * Declares that some metadata cannot be fetched because on incomplete 
<abbr>JDBC</abbr> driver.
+     * This is invoked (indirectly) during {@link Table} construction.
+     * This method tries to avoid long chain of redundant exceptions.
+     *
+     * @param e the exception thrown by the <abbr>JDBC</abbr> driver.
+     */
+    final void unavailableMetadata(final SQLFeatureNotSupportedException e) {
+        if (featureNotSupported == null) {
+            featureNotSupported = e;
+        } else if (!equivalent(featureNotSupported, e)) {
+            for (final Throwable s : featureNotSupported.getSuppressed()) {
+                if (equivalent(s, e)) {
+                    return;
+                }
+            }
+            featureNotSupported.addSuppressed(e);
+        }
+    }
+
+    /**
+     * Returns whether two exceptions are considered equal for the purpose of 
warnings.
+     * Current implementation does not check the stack trace, because it can 
be costly.
+     */
+    private static boolean equivalent(final Throwable e1, final SQLException 
e2) {
+        return e1.getClass() == e2.getClass() && 
Objects.equals(e1.getMessage(), e2.getMessage());
+    }
+
     /**
      * Reports a warning. Duplicated warnings will be ignored.
+     * This is invoked (indirectly) during {@link Table} construction.
      *
      * @param  key       one of {@link Resources.Keys} values.
      * @param  argument  the value to substitute to {0} tag in the warning 
message.
@@ -374,23 +424,14 @@ public final class Analyzer {
         for (final Table table : featureTables.values()) {
             table.setDeferredSearchTables(this, featureTables);
         }
+        if (featureNotSupported != null) {
+            LogRecord record = resources().getLogRecord(Level.WARNING, 
Resources.Keys.CanNotAnalyzeFully);
+            record.setThrown(featureNotSupported);
+            database.log(record);
+        }
         for (final ResourceInternationalString warning : warnings) {
             database.log(warning.toLogRecord(Level.WARNING));
         }
         return featureTables.values();
     }
-
-    /**
-     * Initializes the value getter on the given column.
-     * This method shall be invoked only after geometry columns have been 
identified.
-     */
-    final ValueGetter<?> setValueGetter(final Column column) {
-        ValueGetter<?> getter = database.getMapping(column);
-        if (getter == null) {
-            getter = database.getDefaultMapping();
-            warning(Resources.Keys.UnknownType_1, column.typeName);
-        }
-        column.valueGetter = getter;
-        return getter;
-    }
 }
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 b83f7b6e4d..4ee4872dc0 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
@@ -179,6 +179,13 @@ public class Database<G> extends Syntax  {
      */
     private boolean hasRaster;
 
+    /**
+     * A flag for remembering that {@link SQLFeatureNotSupportedException} has 
already been reported
+     * during a count of the number of features. Used for avoiding to pollute 
the logs with the same
+     * warning repeated many times.
+     */
+    volatile boolean cannotCount;
+
     /**
      * Catalog and schema of the {@code "GEOMETRY_COLUMNS"} and {@code 
"SPATIAL_REF_SYS"} tables,
      * or null or empty string if none. The actual table names depend on 
{@link #spatialSchema}.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java
index a3f3375915..169df707e0 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java
@@ -219,7 +219,7 @@ abstract class FeatureAnalyzer {
         AttributeTypeBuilder<?> attribute = null;
         final boolean created = (isPrimaryKey || dependencies == null);
         if (created) {
-            final ValueGetter<?> getter = analyzer.setValueGetter(column);
+            final ValueGetter<?> getter = analyzer.setValueGetterOf(column);
             attribute = column.createAttribute(feature);
             /*
              * Some columns have special purposes: components of primary keys 
will be used for creating
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
index 7634cb5178..13dea0b1fd 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
@@ -78,6 +78,10 @@ final class FeatureIterator implements Spliterator<Feature>, 
AutoCloseable {
 
     /**
      * Estimated number of remaining rows, or ≤ 0 if unknown.
+     * Zero is considered unknown (instead of only negative values)
+     * because some databases return 0 when they cannot count.
+     *
+     * @see Table#countRows(DatabaseMetaData, boolean, boolean)
      */
     private final long estimatedSize;
 
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 942d8bb9af..aa2f7b8ce3 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
@@ -154,7 +154,7 @@ final class QueryAnalyzer extends FeatureAnalyzer {
     @Override
     Column[] createAttributes() throws Exception {
         /*
-         * Identify geometry columns. Must be done before the calls to 
`setValueGetter(column)`.
+         * Identify geometry columns. Must be done before the calls to 
`Analyzer.setValueGetterOf(column)`.
          */
         final InfoStatements spatialInformation = analyzer.spatialInformation;
         if (spatialInformation != null) {
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
index ccff1af46d..534146c158 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
@@ -58,6 +58,11 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short AssumeUnsigned = 16;
 
+        /**
+         * Cannot analyze fully the database schema because of incomplete 
metadata.
+         */
+        public static final short CanNotAnalyzeFully = 17;
+
         /**
          * Cannot fetch a Coordinate Reference System (CRS) for SRID code {0}.
          */
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
index 66a7c28bb3..77620d600b 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
@@ -20,6 +20,7 @@
 # For resources shared by all modules in the Apache SIS project, see 
"org.apache.sis.util.resources" package.
 #
 AssumeUnsigned                    = Assume database byte/tinyint unsigned, due 
to a lack of metadata.
+CanNotAnalyzeFully                = Cannot analyze fully the database schema 
because of incomplete metadata.
 CanNotFetchCRS_1                  = Cannot fetch a Coordinate Reference System 
(CRS) for SRID code {0}.
 CanNotFindSRID_1                  = Cannot find an identifier in the database 
for the reference system \u201c{0}\u201d.
 DataSource                        = Provider of connections to the database.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
index f7394f8038..437f789ad5 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
@@ -24,7 +24,8 @@
 #   U+202F NARROW NO-BREAK SPACE  before  ; ! and ?
 #   U+00A0 NO-BREAK SPACE         before  :
 #
-AssumeUnsigned                    = Les valeurs de type 
\u2018tinyint\u2019/\u2018byte\u2019 seront consid\u00e9r\u00e9es comme non 
sign\u00e9es, car la base de donn\u00e9es ne fournit pas d'information \u00e0 
ce sujet.
+AssumeUnsigned                    = Les valeurs de type 
\u2018tinyint\u2019/\u2018byte\u2019 seront consid\u00e9r\u00e9es comme non 
sign\u00e9es, car la base de donn\u00e9es ne fournit pas d\u2019information 
\u00e0 ce sujet.
+CanNotAnalyzeFully                = Ne peut pas analyser compl\u00e8tement le 
sch\u00e9ma de la base de donn\u00e9es parce que les m\u00e9ta-donn\u00e9es 
sont incompl\u00e8tes.
 CanNotFetchCRS_1                  = Ne peut pas obtenir un syst\u00e8me de 
r\u00e9f\u00e9rence des coordonn\u00e9es pour le code SRID {0}.
 CanNotFindSRID_1                  = Ne peut pas trouver un identifiant dans la 
base de donn\u00e9es pour le syst\u00e8me de r\u00e9f\u00e9rence 
\u00ab\u202f{0}\u202f\u00bb.
 DataSource                        = Fournisseur de connexions \u00e0 la base 
de donn\u00e9es.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
index 7bb776d5cc..932e3fde96 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
@@ -23,6 +23,7 @@ import java.sql.Connection;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
 import org.opengis.util.GenericName;
 import org.opengis.geometry.Envelope;
 import org.apache.sis.storage.AbstractFeatureSet;
@@ -449,6 +450,11 @@ final class Table extends AbstractFeatureSet {
                     }
                 }
             }
+        } catch (SQLFeatureNotSupportedException e) {
+            if (!database.cannotCount) {
+                database.cannotCount = true;
+                database.listeners.warning(e);
+            }
         }
         return count;
     }
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 5bcdd8dab1..78d4d15b7f 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
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.sql.SQLException;
 import java.sql.ResultSet;
+import java.sql.SQLFeatureNotSupportedException;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.metadata.sql.privy.Reflection;
 import org.apache.sis.util.privy.Strings;
@@ -76,6 +77,8 @@ final class TableAnalyzer extends FeatureAnalyzer {
             while (reflect.next()) {
                 primaryKey.add(analyzer.getUniqueString(reflect, 
Reflection.COLUMN_NAME));
             }
+        } catch (SQLFeatureNotSupportedException e) {
+            analyzer.unavailableMetadata(e);
         }
         /*
          * Note: when a table contains no primary keys, we could still look 
for index columns
@@ -133,6 +136,14 @@ final class TableAnalyzer extends FeatureAnalyzer {
                 }
                 relations.add(relation);
             } while (!reflect.isClosed());
+        } catch (SQLFeatureNotSupportedException e) {
+            /*
+             * Some database implementations cannot not provide information 
about foreigner keys.
+             * We consider this limitation as non-fatal. The users will still 
see the table that,
+             * only their dependencies will not be visible. Instead, the 
foreigner key will appear
+             * as an ordinary attribute value.
+             */
+            analyzer.unavailableMetadata(e);
         }
         final int size = relations.size();
         return (size != 0) ? relations.toArray(new Relation[size]) : 
Relation.EMPTY;

Reply via email to