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 d4bba49f28907f4c08d7e33ee4a80746786f999f
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Mar 18 12:46:15 2025 +0100

    Make the search for database metadata more robust to drivers that do not 
define an escape character.
---
 .../apache/sis/metadata/sql/privy/SQLBuilder.java  | 20 ++++---
 .../sis/metadata/sql/privy/SQLUtilities.java       | 39 ++++++++------
 .../org/apache/sis/metadata/sql/privy/Syntax.java  | 47 +++++++++++++++-
 .../apache/sis/storage/sql/feature/Analyzer.java   |  8 ---
 .../apache/sis/storage/sql/feature/Database.java   | 63 +++++++++++++---------
 .../sis/storage/sql/feature/FeatureIterator.java   |  5 +-
 .../sis/storage/sql/feature/FeatureStream.java     |  4 +-
 .../sis/storage/sql/feature/InfoStatements.java    |  2 +-
 .../storage/sql/feature/SelectionClauseWriter.java |  7 ++-
 .../sis/storage/sql/feature/TableAnalyzer.java     | 48 ++++++++++-------
 .../apache/sis/io/stream/InternalOptionKey.java    |  2 +-
 11 files changed, 157 insertions(+), 88 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
index cae7decee6..9c8c60629f 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
@@ -299,6 +299,8 @@ public class SQLBuilder extends Syntax {
     /**
      * Appends a string as an escaped {@code LIKE} argument.
      * This method does not put any {@code '} character, and does not accept 
null argument.
+     * If the database does not have predefined wildcard characters, then the 
query result
+     * may contain false positives.
      *
      * <p>This method does not double the simple quotes of the given string on 
intent, because
      * it may be used in a {@code PreparedStatement}. If the simple quotes 
need to be doubled,
@@ -306,14 +308,20 @@ public class SQLBuilder extends Syntax {
      *
      * @param  value  the value to append.
      * @return this builder, for method call chaining.
+     *
+     * @see #escapeWildcards(String)
      */
     public final SQLBuilder appendWildcardEscaped(final String value) {
-        final int start = buffer.length();
-        buffer.append(value);
-        for (int i = buffer.length(); --i >= start;) {
-            final char c = buffer.charAt(i);
-            if (c == '_' || c == '%') {
-                buffer.insert(i, escape);
+        if (cannotEscapeWildcards()) {
+            buffer.append(value.replace("%", "_"));
+        } else {
+            final int start = buffer.length();
+            buffer.append(value);
+            for (int i = buffer.length(); --i >= start;) {
+                final char c = buffer.charAt(i);
+                if (c == '_' || c == '%') {
+                    buffer.insert(i, escape);
+                }
             }
         }
         return this;
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java
index c3196ad286..4e400eeaa9 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java
@@ -91,28 +91,37 @@ public final class SQLUtilities extends Static {
     }
 
     /**
-     * Returns the given pattern with {@code '_'} and {@code '%'} characters 
escaped by the database-specific
+     * Returns the given text with {@code '_'} and {@code '%'} characters 
escaped by the database-specific
      * escape characters. This method should be invoked for escaping the 
values of all {@link DatabaseMetaData}
-     * method arguments with a name ending by {@code "Pattern"}. Note that not 
all arguments are pattern; please
-     * checks carefully {@link DatabaseMetaData} javadoc for each method.
+     * method arguments having a name ending by {@code "Pattern"}. Note that 
not all arguments are patterns,
+     * please check carefully the {@link DatabaseMetaData} javadoc for each 
method.
      *
      * <h4>Example</h4>
-     * If a method expects an argument named {@code tableNamePattern},
-     * then that argument value should be escaped. But if the argument name is 
only {@code tableName},
-     * then the value should not be escaped.
+     * If a method expects an argument named {@code tableNamePattern}, then 
the value should be escaped
+     * if an exact match is desired. But if the argument name is only {@code 
tableName}, then the value
+     * should not be escaped.
      *
-     * @param  pattern  the pattern to escape, or {@code null} if none.
-     * @param  escape   value of {@link 
DatabaseMetaData#getSearchStringEscape()}.
-     * @return escaped strings, or the same instance as {@code pattern} if 
there are no characters to escape.
+     * <h4>Missing escape characters</h4>
+     * Some databases do not provide an escape character. If the given {@code 
escape} is null or empty,
+     * then instead of escaping, this method will replace all occurrences of 
{@code '%'} by {@code '_'}.
+     * It will cause the database to return more metadata rows that desired. 
Callers should filter by
+     * comparing the table and schema name specified in each row against the 
original {@code name}.
+     *
+     * @param  text    the text to escape for use in a context equivalent to 
the {@code LIKE} statement.
+     * @param  escape  value of {@link 
DatabaseMetaData#getSearchStringEscape()}. May be null or empty.
+     * @return the given text with wildcard characters escaped.
      */
-    public static String escape(final String pattern, final String escape) {
-        if (pattern != null) {
+    public static String escape(final String text, final String escape) {
+        if (text != null) {
+            if (escape == null || escape.isEmpty()) {
+                return text.replace("%", "_");
+            }
             StringBuilder buffer = null;
-            for (int i = pattern.length(); --i >= 0;) {
-                final char c = pattern.charAt(i);
+            for (int i = text.length(); --i >= 0;) {
+                final char c = text.charAt(i);
                 if (c == '_' || c == '%') {
                     if (buffer == null) {
-                        buffer = new StringBuilder(pattern);
+                        buffer = new StringBuilder(text);
                     }
                     buffer.insert(i, escape);
                 }
@@ -121,7 +130,7 @@ public final class SQLUtilities extends Static {
                 return buffer.toString();
             }
         }
-        return pattern;
+        return text;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
index d4ab856b54..8021ffe8dd 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java
@@ -50,8 +50,14 @@ public class Syntax {
     /**
      * The string that can be used to escape wildcard characters.
      * This is the value returned by {@link 
DatabaseMetaData#getSearchStringEscape()}.
+     * It may be null or empty if the database has no escape character, in 
which case
+     * the statement should be of the form {@code WHERE "column" LIKE ? ESCAPE 
'\'}
+     * (replace {@code '\'} by the desired escape character).
+     *
+     * @see #escapeWildcards(String)
+     * @see SQLBuilder#appendWildcardEscaped(String)
      */
-    protected final String escape;
+    final String escape;
 
     /**
      * Creates a new {@code Syntax} initialized from the given database 
metadata.
@@ -68,7 +74,7 @@ public class Syntax {
         } else {
             dialect = Dialect.ANSI;
             quote   = "\"";
-            escape  = "\\";
+            escape  = null;
         }
         this.quoteSchema = quoteSchema;
     }
@@ -84,4 +90,41 @@ public class Syntax {
         quote       = other.quote;
         quoteSchema = other.quoteSchema;
     }
+
+    /**
+     * Returns the given text with {@code '_'} and {@code '%'} characters 
escaped by the database-specific
+     * escape characters. This method should be invoked for escaping the 
values of all {@link DatabaseMetaData}
+     * method arguments having a name ending by {@code "Pattern"}. Note that 
not all arguments are patterns,
+     * please check carefully the {@link DatabaseMetaData} javadoc for each 
method.
+     *
+     * <h4>Example</h4>
+     * If a method expects an argument named {@code tableNamePattern}, then 
the value should be escaped
+     * if an exact match is desired. But if the argument name is only {@code 
tableName}, then the value
+     * should not be escaped.
+     *
+     * <h4>Missing escape characters</h4>
+     * Some databases do not provide an escape character. If the given {@code 
escape} is null or empty,
+     * then instead of escaping, this method will replace all occurrences of 
{@code '%'} by {@code '_'}.
+     * It will cause the database to return more metadata rows that desired. 
Callers should filter by
+     * comparing the table and schema name specified in each row against the 
original {@code name}.
+     *
+     * @param  text  the text to escape for use in a context equivalent to the 
{@code LIKE} statement.
+     * @return the given text with wildcard characters escaped.
+     */
+    public final String escapeWildcards(final String text) {
+        return SQLUtilities.escape(text, escape);
+    }
+
+    /**
+     * Returns {@code true} if the database can <em>not</em> escape wildcard 
characters.
+     * In such case, the string returned by {@link #escapeWildcards(String)} 
may produce
+     * false positive, and the callers need to apply additional filtering.
+     *
+     * <p>This is rarely true, but may happen with incomplete 
<abbr>JDBC</abbr> drivers.</p>
+     *
+     * @return whether the database can <em>not</em> escape wildcard 
characters.
+     */
+    public final boolean cannotEscapeWildcards() {
+        return (escape == null) || escape.isEmpty();
+    }
 }
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 4e6cec6d49..a8a6e0fd4d 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
@@ -97,13 +97,6 @@ public final class Analyzer {
      */
     private final Map<String,String> uniqueStrings;
 
-    /**
-     * The string to insert before wildcard characters ({@code '_'} or {@code 
'%'}) to escape.
-     * This is used by {@link #escape(String)} before to pass argument values 
(e.g. table name)
-     * to {@link DatabaseMetaData} methods expecting a pattern.
-     */
-    final String escape;
-
     /**
      * The "TABLE" and "VIEW" keywords for table types, with unsupported 
keywords omitted.
      */
@@ -163,7 +156,6 @@ public final class Analyzer {
             throws Exception
     {
         this.metadata      = metadata;
-        this.escape        = metadata.getSearchStringEscape();
         this.nameFactory   = DefaultNameFactory.provider();
         this.featureTables = new HashMap<>();
         this.uniqueStrings = new HashMap<>();
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 e61f12d6ba..b83f7b6e4d 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
@@ -43,7 +43,6 @@ import org.apache.sis.metadata.sql.privy.Syntax;
 import org.apache.sis.metadata.sql.privy.Dialect;
 import org.apache.sis.metadata.sql.privy.Reflection;
 import org.apache.sis.metadata.sql.privy.SQLBuilder;
-import org.apache.sis.metadata.sql.privy.SQLUtilities;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.FeatureNaming;
 import org.apache.sis.storage.DataStoreException;
@@ -80,7 +79,7 @@ import org.opengis.metadata.citation.PresentationForm;
  * specialized methods or have non-standard behavior for some data types.</p>
  *
  * <h2>Specializations</h2>
- * Subclasses may be defined for some database engines. Methods that can be 
overridden are:
+ * Subclasses may be defined for some specific database drivers. Methods that 
can be overridden are:
  * <ul>
  *   <li>{@link #getPossibleSpatialSchemas(Map)}    for enumerating the 
spatial schema conventions that may be used.</li>
  *   <li>{@link #getMapping(Column)}                for adding column types to 
recognize.</li>
@@ -327,18 +326,6 @@ public class Database<G> extends Syntax  {
         return Collections.unmodifiableMap(softwareVersions);
     }
 
-    /**
-     * Returns the value of a {@code LIKE} statement with wildcard characters 
escaped.
-     * The wildcard characters are {@code '_'} and {@code '%'}. This method is 
invoked
-     * when an exact match for the given value is desired.
-     *
-     * @param  value  the {@code LIKE} pattern to search.
-     * @return the given pattern with wildcard characters escaped.
-     */
-    final String escapeWildcards(final String value) {
-        return SQLUtilities.escape(value, escape);
-    }
-
     /**
      * Detects automatically which spatial schema is in use. Detects also the 
catalog name and schema name.
      * This method is invoked exactly once after construction and before the 
analysis of feature tables.
@@ -358,6 +345,7 @@ public class Database<G> extends Syntax  {
          */
         String crsTable = null;
         final var ignoredTables = new HashMap<String,Boolean>(8);
+        final boolean isSearchReliable = !cannotEscapeWildcards();
         final SpatialSchema[] candidates = 
getPossibleSpatialSchemas(ignoredTables);
         for (int i=0; i<candidates.length; i++) {
             final SpatialSchema convention = candidates[i];
@@ -386,13 +374,16 @@ public class Database<G> extends Syntax  {
                 // Unconditionally check table existence during the first 
iteration.
                 if (i == 0 || entry.getValue() == null) {
                     boolean exists = false;
-                    String table = escapeWildcards(entry.getKey());
-                    try (ResultSet reflect = metadata.getTables(null, null, 
table, tableTypes)) {
+                    final String table = entry.getKey();
+                    try (ResultSet reflect = metadata.getTables(null, null, 
escapeWildcards(table), tableTypes)) {
                         while (reflect.next()) {
-                            consistent &= consistent(catalog, catalog = 
reflect.getString(Reflection.TABLE_CAT));
-                            consistent &= consistent(schema,  schema  = 
reflect.getString(Reflection.TABLE_SCHEM));
-                            found |= !Boolean.FALSE.equals(entry.getValue());  
// Accept `true` and `null` values.
-                            exists = true;
+                            // Double-check of the table name because not all 
software can escape wildcards.
+                            if (isSearchReliable || 
table.equals(reflect.getString(Reflection.TABLE_NAME))) {
+                                consistent &= consistent(catalog, catalog = 
reflect.getString(Reflection.TABLE_CAT));
+                                consistent &= consistent(schema,  schema  = 
reflect.getString(Reflection.TABLE_SCHEM));
+                                found |= 
!Boolean.FALSE.equals(entry.getValue());  // Accept `true` and `null` values.
+                                exists = true;
+                            }
                         }
                     }
                     entry.setValue(exists);
@@ -415,17 +406,21 @@ public class Database<G> extends Syntax  {
          * The preference order will be defined by the `CRSEncoding` 
enumeration order.
          */
         if (spatialSchema != null) {
-            final String schema = escapeWildcards(schemaOfSpatialTables);
-            final String table  = escapeWildcards(crsTable);
             for (Map.Entry<CRSEncoding, String> entry : 
spatialSchema.crsDefinitionColumn.entrySet()) {
                 String column = entry.getValue();
                 if (metadata.storesLowerCaseIdentifiers()) {
                     column = column.toLowerCase(Locale.US);
                 }
-                column = escapeWildcards(column);
-                try (ResultSet reflect = 
metadata.getColumns(catalogOfSpatialTables, schema, table, column)) {
-                    if (reflect.next()) {
-                        crsEncodings.add(entry.getKey());
+                try (ResultSet reflect = 
metadata.getColumns(catalogOfSpatialTables,    // No escape for this argument.
+                                             
escapeWildcards(schemaOfSpatialTables),
+                                             escapeWildcards(crsTable),
+                                             escapeWildcards(column)))
+                {
+                    while (reflect.next()) {
+                        if (isSearchReliable || filterMetadata(reflect, 
schemaOfSpatialTables, crsTable, column)) {
+                            crsEncodings.add(entry.getKey());
+                            break;
+                        }
                     }
                 }
             }
@@ -572,6 +567,22 @@ public class Database<G> extends Syntax  {
         sql.append(name);
     }
 
+    /**
+     * Double-checks whether the metadata about a table or a column are for 
the item that we requested.
+     * We perform this double check because some database drivers have no 
predefined escape characters
+     * for wildcards. If any {@code String} argument is {@code null} or empty, 
it will be ignored.
+     *
+     * <p>Note that the catalog is not verified because the {@code catalog} 
argument in
+     * {@link DatabaseMetaData} is not a pattern.</p>
+     */
+    static boolean filterMetadata(ResultSet reflect, String schema, String 
table, String column)
+            throws SQLException
+    {
+        return (Strings.isNullOrEmpty(schema)  ||  
schema.equals(reflect.getString(Reflection.TABLE_SCHEM))) &&
+               (Strings.isNullOrEmpty(table)   ||   
table.equals(reflect.getString(Reflection.TABLE_NAME)))  &&
+               (Strings.isNullOrEmpty(column)  ||  
column.equals(reflect.getString(Reflection.COLUMN_NAME)));
+    }
+
     /**
      * Returns an identification of the table and column naming conventions.
      * This is absent if the database is not spatial.
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 ba0836a33d..7634cb5178 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
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.storage.sql.feature;
 
-import java.util.List;
 import java.util.ArrayList;
 import java.util.Spliterator;
 import java.util.function.Consumer;
@@ -121,7 +120,7 @@ final class FeatureIterator implements 
Spliterator<Feature>, AutoCloseable {
                 ? table.database.createInfoStatements(connection) : null;
         String sql = adapter.sql;
         if (distinct || filter != null || sort != null || offset > 0 || count 
> 0) {
-            final SQLBuilder builder = new 
SQLBuilder(table.database).append(sql);
+            final var builder = new SQLBuilder(table.database).append(sql);
             if (distinct) {
                 builder.insertDistinctAfterSelect();
             }
@@ -293,7 +292,7 @@ final class FeatureIterator implements 
Spliterator<Feature>, AutoCloseable {
      * @return the feature as a singleton {@code Feature} or as a {@code 
Collection<Feature>}.
      */
     private Object fetchReferenced(final Feature owner) throws Exception {
-        final List<Feature> features = new ArrayList<>();
+        final var features = new ArrayList<Feature>();
         try (ResultSet r = statement.executeQuery()) {
             result = r;
             fetch(features::add, true);
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
index f1485ce02f..c24daa5e88 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
@@ -311,7 +311,7 @@ final class FeatureStream extends DeferredStream<Feature> {
          * Build the full SQL statement here, without using 
`FeatureAdapter.sql`,
          * because we do not need to follow foreigner keys.
          */
-        final SQLBuilder sql = new 
SQLBuilder(table.database).append(SQLBuilder.SELECT).append("COUNT(");
+        final var sql = new 
SQLBuilder(table.database).append(SQLBuilder.SELECT).append("COUNT(");
         if (distinct) {
             String separator = "DISTINCT ";
             for (final Column attribute : table.attributes) {
@@ -401,7 +401,7 @@ final class FeatureStream extends DeferredStream<Feature> {
         final Connection connection = getConnection();
         setCloseHandler(connection);  // Executed only if `FeatureIterator` 
creation fails, discarded later otherwise.
         makeReadOnly(connection);
-        final FeatureIterator features = new FeatureIterator(table, 
connection, distinct, filter, sort, offset, count);
+        final var features = new FeatureIterator(table, connection, distinct, 
filter, sort, offset, count);
         setCloseHandler(features);
         return features;
     }
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 ce4c1dbfb1..f8590e8d43 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
@@ -236,7 +236,7 @@ public class InfoStatements implements Localized, 
AutoCloseable {
     /**
      * Sets the parameter value for a table catalog or schema. Those 
parameters use the {@code LIKE} statement
      * in order to ignore the catalog or schema when it is not specified. A 
catalog or schema is not specified
-     * if the string is null or empty. The latter case may happens with some 
drivers with, for example,
+     * if the string is null or empty. The latter case may happen with some 
drivers with, for example,
      * materialized views.
      *
      * @param  columnQuery  the query where to set the parameter.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java
index d575e0a8c0..a47317c3ca 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java
@@ -17,7 +17,6 @@
 package org.apache.sis.storage.sql.feature;
 
 import java.util.List;
-import java.util.Map;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.function.BiConsumer;
@@ -150,7 +149,7 @@ public class SelectionClauseWriter extends Visitor<Feature, 
SelectionClause> {
      * @return a writer with unsupported functions removed.
      */
     final SelectionClauseWriter removeUnsupportedFunctions(final Database<?> 
database) {
-        final Map<String,SpatialOperatorName> unsupported = new HashMap<>();
+        final var unsupported = new HashMap<String, SpatialOperatorName>();
         try (Connection c = database.source.getConnection()) {
             final DatabaseMetaData metadata = c.getMetaData();
             /*
@@ -173,9 +172,9 @@ public class SelectionClauseWriter extends Visitor<Feature, 
SelectionClause> {
              * Remove from above map all functions that are supported by the 
database.
              * This list is potentially large so we do not put those items in 
a map.
              */
-            final String pattern = (lowerCase ? "st_%" : 
"ST\\_%").replace("\\", metadata.getSearchStringEscape());
+            final String prefix = database.escapeWildcards(lowerCase ? "st_" : 
"ST_");
             try (ResultSet r = 
metadata.getFunctions(database.catalogOfSpatialTables,
-                                                     
database.schemaOfSpatialTables, pattern))
+                                                     
database.schemaOfSpatialTables, prefix + '%'))
             {
                 while (r.next()) {
                     unsupported.remove(r.getString("FUNCTION_NAME"));
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 043923ec66..5bcdd8dab1 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
@@ -22,7 +22,6 @@ import java.sql.SQLException;
 import java.sql.ResultSet;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.metadata.sql.privy.Reflection;
-import org.apache.sis.metadata.sql.privy.SQLUtilities;
 import org.apache.sis.util.privy.Strings;
 
 
@@ -47,6 +46,13 @@ final class TableAnalyzer extends FeatureAnalyzer {
      */
     private final String tableEsc, schemaEsc;
 
+    /**
+     * Whether the filtering by schema name and table name is reliable.
+     * This is usually true, but may be false with incomplete drivers
+     * that do not declare search escape characters.
+     */
+    private final boolean isSearchReliable;
+
     /**
      * Creates an analyzer for the table of the given name.
      * The table is identified by {@code id}, which contains a (catalog, 
schema, name) tuple.
@@ -57,11 +63,15 @@ final class TableAnalyzer extends FeatureAnalyzer {
      *                       the table that "contains" this table. Otherwise 
{@code null}.
      * @throws SQLException if an error occurred while fetching information 
from the database.
      */
+    @SuppressWarnings("StringEquality")
     TableAnalyzer(final Analyzer analyzer, final TableReference id, final 
TableReference dependencyOf) throws SQLException {
         super(analyzer, id);
         this.dependencyOf = dependencyOf;
-        this.tableEsc     = escape(id.table);
-        this.schemaEsc    = escape(id.schema);
+        this.tableEsc     = analyzer.database.escapeWildcards(id.table);
+        this.schemaEsc    = analyzer.database.escapeWildcards(id.schema);
+        isSearchReliable = !analyzer.database.cannotEscapeWildcards()
+                || (tableEsc == id.table && schemaEsc == id.schema);    // 
Identity checks are okay.
+
         try (ResultSet reflect = analyzer.metadata.getPrimaryKeys(id.catalog, 
id.schema, id.table)) {
             while (reflect.next()) {
                 primaryKey.add(analyzer.getUniqueString(reflect, 
Reflection.COLUMN_NAME));
@@ -76,18 +86,12 @@ final class TableAnalyzer extends FeatureAnalyzer {
     }
 
     /**
-     * Returns the given pattern with {@code '_'} and {@code '%'} characters 
escaped by the database-specific
-     * escape characters. This method should be invoked for escaping the 
values of all {@link DatabaseMetaData}
-     * method arguments with a name ending by {@code "Pattern"}. Note that not 
all arguments are pattern; please
-     * checks carefully {@link DatabaseMetaData} javadoc for each method.
-     *
-     * <h4>Example</h4>
-     * If a method expects an argument named {@code tableNamePattern},
-     * then that argument value should be escaped. But if the argument name is 
only {@code tableName},
-     * then the value should not be escaped.
+     * Double-checks whether the metadata about a table are for the item that 
we requested.
+     * We perform this double check because some database drivers have no 
predefined escape
+     * characters for wildcards.
      */
-    private String escape(final String pattern) {
-        return SQLUtilities.escape(pattern, analyzer.escape);
+    private boolean filterMetadata(final ResultSet reflect) throws 
SQLException {
+        return isSearchReliable || Database.filterMetadata(reflect, id.schema, 
id.table, null);
     }
 
     /**
@@ -155,9 +159,11 @@ final class TableAnalyzer extends FeatureAnalyzer {
         final String quote = analyzer.metadata.getIdentifierQuoteString();
         try (ResultSet reflect = analyzer.metadata.getColumns(id.catalog, 
schemaEsc, tableEsc, null)) {
             while (reflect.next()) {
-                final var column = new Column(analyzer, reflect, quote);
-                if (columns.put(column.name, column) != null) {
-                    throw duplicatedColumn(column);
+                if (filterMetadata(reflect)) {
+                    final var column = new Column(analyzer, reflect, quote);
+                    if (columns.put(column.name, column) != null) {
+                        throw duplicatedColumn(column);
+                    }
                 }
             }
         }
@@ -185,9 +191,11 @@ final class TableAnalyzer extends FeatureAnalyzer {
         if (id instanceof Relation) {
             try (ResultSet reflect = analyzer.metadata.getTables(id.catalog, 
schemaEsc, tableEsc, null)) {
                 while (reflect.next()) {
-                    final String remarks = 
Strings.trimOrNull(analyzer.getUniqueString(reflect, Reflection.REMARKS));
-                    if (remarks != null) {
-                        return remarks;
+                    if (filterMetadata(reflect)) {
+                        String remarks = 
Strings.trimOrNull(analyzer.getUniqueString(reflect, Reflection.REMARKS));
+                        if (remarks != null) {
+                            return remarks;
+                        }
                     }
                 }
             }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InternalOptionKey.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InternalOptionKey.java
index b34ff79a7d..f36ac483f2 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InternalOptionKey.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InternalOptionKey.java
@@ -58,7 +58,7 @@ public final class InternalOptionKey<T> extends OptionKey<T> {
     /**
      * The lock to use in a data store when those locks are optional. For 
example, data stores on
      * <abbr>SQL</abbr> databases should not need locks because 
<abbr>ACID</abbr>-compliant databases
-     * should support thread-safe transactions. However, some database 
products do not provide the
+     * should support thread-safe transactions. However, some database drivers 
do not provide the
      * expected thread-safety, in which case Apache <abbr>SIS</abbr> may need 
to do locking itself.
      */
     public static final InternalOptionKey<ReadWriteLock> LOCKS =

Reply via email to