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;