This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 27beef8199f07ad2e458967ee722f6dd2d2c8c12 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Oct 17 00:33:40 2026 +0200 Partial workaround for the problem of HSQL which ignore collations in LIKE clause. We cannot compensate fully this problem, but we can make it less frequent. --- .../sis/metadata/sql/internal/shared/Dialect.java | 11 ++++ .../metadata/sql/internal/shared/SQLBuilder.java | 3 +- .../metadata/sql/internal/shared/ScriptRunner.java | 4 +- .../sis/metadata/sql/internal/shared/Supports.java | 8 ++- .../factory/ConcurrentAuthorityFactory.java | 2 +- .../factory/IdentifiedObjectFinder.java | 2 +- .../referencing/factory/sql/EPSGCodeFinder.java | 61 ++++++++++++++++------ .../sis/referencing/factory/sql/EPSGInstaller.java | 4 ++ .../sis/referencing/factory/sql/SQLTranslator.java | 16 +++++- .../sis/referencing/factory/sql/package-info.java | 2 +- .../main/org/apache/sis/pending/jdk/JDK26.java | 41 +++++++++++++++ 11 files changed, 130 insertions(+), 24 deletions(-) diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Dialect.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Dialect.java index adbf5d1e63..ff2a4ee83e 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Dialect.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Dialect.java @@ -49,6 +49,7 @@ public enum Dialect { * <a href="https://issues.apache.org/jira/browse/DERBY-6445">DERBY-6445</a> */ DERBY("derby", Supports.ALTER_TABLE_WITH_ADD_CONSTRAINT + | Supports.COLLATION_IN_LIKE | Supports.READ_ONLY_UPDATE | Supports.CONCURRENCY), @@ -82,6 +83,7 @@ public enum Dialect { | Supports.CONCURRENCY | Supports.JAVA_TIME | Supports.ILIKE + | Supports.COLLATION_IN_LIKE // Since PostgreSQL 18. | Supports.SRID), /** @@ -271,6 +273,15 @@ public enum Dialect { return (flags & Supports.SRID) != 0; } + /** + * Whether the database takes collation in account in {@code LIKE} clauses. + * + * @return whether the database supports collations in {@code LIKE} clauses. + */ + public final boolean supportsCollationInLike() { + return (flags & Supports.COLLATION_IN_LIKE) != 0; + } + /** * Returns the keyword for a case-insensitive {@code LIKE}, or {@code null} if none. * The default implementation returns {@code null} because {@code ILIKE} is not standard <abbr>SQL</abbr>. diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/SQLBuilder.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/SQLBuilder.java index 2455bff4f5..2fdd2fbffd 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/SQLBuilder.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/SQLBuilder.java @@ -258,7 +258,7 @@ public class SQLBuilder extends Syntax { /** * Appends a {@code "= <value>"} string in a {@code SELECT} statement. - * The value is written between quotes, except if it is a number or a boolean. + * The value is written between quotes, except if it is a number or a Boolean. * * @param value the value to append, or {@code null}. * @return this builder, for method call chaining. @@ -278,6 +278,7 @@ public class SQLBuilder extends Syntax { * @return this builder, for method call chaining. */ public final SQLBuilder appendValue(final String value) { + // TODO: use Connection.enquoteLiteral(String) with JDK26. return (value == null) ? append(NULL) : append('\'').append(value.replace("'", "''")).append('\''); } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/ScriptRunner.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/ScriptRunner.java index 84ab3300d0..cf7ba6d13a 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/ScriptRunner.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/ScriptRunner.java @@ -178,8 +178,8 @@ public class ScriptRunner implements AutoCloseable { * In principle, {@link Connection#rollback()} should be sufficient, but it appears to not be the case * with all databases. * - * <p>This is a workaround for what seems to be a bug in <abbr>HSQLDB</abbr>, where - * {@link Connection#rollback()} does drop the schema created before the rollback.</p> + * <p>This is a workaround for a limitation in <abbr>HSQLDB</abbr>, where + * {@link Connection#rollback()} does not drop the schema created before the rollback.</p> * * @see <a href="https://sourceforge.net/p/hsqldb/bugs/1754/">Connection.rollback() does not remove schemas</a> */ diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Supports.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Supports.java index bf4b3e9765..5bd73cec98 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Supports.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/internal/shared/Supports.java @@ -86,10 +86,16 @@ final class Supports { */ public static final int ILIKE = 0x200; + /** + * Whether the database supports collations in {@code LIKE} clauses. + * Some databases support collations in {@code =} comparisons but silently ignore it in {@code LIKE} clauses. + */ + public static final int COLLATION_IN_LIKE = 0x400; + /** * Whether the spatial extension supports <abbr>SRID</abbr> in {@code ST_*} functions. */ - public static final int SRID = 0x400; + public static final int SRID = 0x800; /** * Do not allow instantiation of this class. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java index 32cf7fb30d..dbd98f44c4 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java @@ -300,7 +300,7 @@ public abstract class ConcurrentAuthorityFactory<DAO extends GeodeticAuthorityFa * If more than this number of threads are querying this {@code ConcurrentAuthorityFactory} concurrently, * additional threads will be blocked until a Data Access Object become available. */ - @SuppressWarnings("this-escape") // Phantom reference. + @SuppressWarnings({"this-escape", "LeakingThisInConstructor"}) // Phantom reference. protected ConcurrentAuthorityFactory(final Class<DAO> dataAccessClass, final int maxStrongReferences, final int maxConcurrentQueries) { diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java index 49c7bd77f8..aed8f8eaf0 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java @@ -447,7 +447,7 @@ public class IdentifiedObjectFinder { /** Snapshot of {@link IdentifiedObjectFinder#proxy}. */ private final AuthorityFactoryProxy<?> proxy; - /** The authority codes form which to create object candidates. */ + /** The authority codes from which to create object candidates. */ private final Iterable<String> codes; /** The comparison mode for deciding if a candidate is a match. */ diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java index 36f13b09a4..3f8040eb23 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java @@ -16,10 +16,12 @@ */ package org.apache.sis.referencing.factory.sql; +import java.util.Map; import java.util.Set; import java.util.List; import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.LinkedHashMap; import java.util.Collection; import java.util.Iterator; import java.sql.Statement; @@ -54,6 +56,7 @@ import org.apache.sis.util.collection.Containers; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.pending.jdk.JDK16; import org.apache.sis.pending.jdk.JDK19; +import org.apache.sis.pending.jdk.JDK26; import org.apache.sis.metadata.internal.shared.ReferencingServices; import org.apache.sis.metadata.iso.citation.Citations; import org.apache.sis.referencing.IdentifiedObjects; @@ -434,20 +437,22 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { * so that buffer shall not contain valuable information yet. */ final var buffer = new StringBuilder(350); // Temporary buffer for building SQL query. - final Set<String> namePatterns; // Name including aliases. + final Map<String, String> namePatterns; // Name and aliases as keys, LIKE patterns as values. final String aliasSQL; if (ArraysExt.containsIdentity(filters, Condition.NAME)) { - namePatterns = new LinkedHashSet<>(); - namePatterns.add(toDatumPattern(getName(object), buffer)); - for (final GenericName id : object.getAlias()) { - namePatterns.add(toDatumPattern(id.tip().toString(), buffer)); + namePatterns = new LinkedHashMap<>(); + String name = getName(object); + namePatterns.put(name, toDatumPattern(name, buffer)); + for (final GenericName alias : object.getAlias()) { + name = alias.tip().toString(); + namePatterns.put(name, toDatumPattern(name, buffer)); } buffer.setLength(0); buffer.append("SELECT OBJECT_CODE FROM \"Alias\" WHERE OBJECT_TABLE_NAME='") .append(dao.translator.toActualTableName(source.table)) .append("' AND "); // PostgreSQL does not require explicit cast when the value is a literal instead of "?". - appendLikeNames(namePatterns, "ALIAS", buffer); + appendLikeNames(namePatterns, "ALIAS", dao.translator.supportsCollationInLike, buffer); aliasSQL = dao.translator.apply(buffer.toString()); buffer.setLength(0); } else { @@ -480,9 +485,11 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { */ try (Statement stmt = dao.connection.createStatement()) { if (namePatterns != null) { - if (isNext) buffer.append(" AND "); - isNext = false; - appendLikeNames(namePatterns, source.nameColumn, buffer); + if (isNext) { + isNext = false; + buffer.append(" AND "); + } + appendLikeNames(namePatterns, source.nameColumn, dao.translator.supportsCollationInLike, buffer); try (ResultSet result = stmt.executeQuery(aliasSQL)) { while (result.next()) { final int code = result.getInt(1); @@ -594,18 +601,42 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { * (<column> LIKE '<pattern>' OR …) * } * + * Theoretically, using only the above {@code LIKE} conditions should be sufficient. + * However, some databases support collations only during comparisons with {@code =} + * and ignores collations when using {@code LIKE}. If the databases is known to have + * this limitation, the {@code supportsCollationInLike} flag should be {@code false} + * for writing all conditions twice: once with {@code =} and once with {@code LIKE}. + * This duplication can mitigate the problem, but not fully resolve it. + * * This method assumes that {@code namePatterns} contains at least one element. * * @param namePatterns the patterns created by {@link #toDatumPattern(String, StringBuilder)}. * @param column column where the search for the names. * @param buffer buffer where to add the SQL fragment. */ - private static void appendLikeNames(final Set<String> namePatterns, final String column, final StringBuilder buffer) { - String separator = "("; - for (final String pattern : namePatterns) { - buffer.append(separator).append(column).append(" LIKE '").append(pattern).append('\''); - separator = " OR "; - } + private static void appendLikeNames(final Map<String, String> namePatterns, final String column, + boolean supportsCollationInLike, final StringBuilder buffer) + { + buffer.append('('); + boolean isNext = false; + do { + for (final Map.Entry<String, String> entry : namePatterns.entrySet()) { + if (isNext) { + buffer.append(" OR "); + } + final String name; + final String operator; + if (supportsCollationInLike) { + name = entry.getValue(); // LIKE pattern. + operator = " LIKE "; + } else { + name = entry.getKey(); // Original name. + operator = " = "; + } + buffer.append(column).append(operator).append(JDK26.enquoteLiteral(name)); + isNext = true; + } + } while ((supportsCollationInLike = !supportsCollationInLike) == true); buffer.append(')'); } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java index cd8f9a434c..5ffe3d52ee 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java @@ -63,10 +63,14 @@ final class EPSGInstaller extends ScriptRunner { /** * Creates a new runner which will execute the statements using the given connection. * This constructor creates immediately a schema of the given name in the database. + * Replacements are prepared for features not supported by the target database, + * such as enumerations and collations. * * @param connection the connection to the database. * @param schema the schema, or {@code null} for any, or empty for none. * @throws SQLException if an error occurred while executing a SQL statement. + * + * @see <a href="https://hsqldb.org/doc/guide/databaseobjects-chapt.html#dbc_collations">HSQLDB collations</a> */ public EPSGInstaller(final Connection connection, final String schema) throws SQLException { super(connection, schema == null ? Constants.EPSG : schema.isEmpty() ? null : schema, 100); diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java index 11bf562de0..de2b2b0003 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java @@ -26,11 +26,13 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import org.apache.sis.util.Workaround; import org.apache.sis.util.CharSequences; import org.apache.sis.util.OptionalCandidate; import org.apache.sis.util.StringBuilders; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.internal.shared.Constants; +import org.apache.sis.metadata.sql.internal.shared.Dialect; import org.apache.sis.metadata.sql.internal.shared.Reflection; import org.apache.sis.metadata.sql.internal.shared.SQLUtilities; import org.apache.sis.referencing.internal.Resources; @@ -202,7 +204,7 @@ public class SQLTranslator implements UnaryOperator<String> { * and values are the names used in the <abbr>EPSG</abbr> database on which {@link EPSGDataAccess} is connected. * By convention, all column names are in upper-case while table names are in mixed-case characters.</p> */ - private Map<String,String> tableRewording; + private Map<String, String> tableRewording; /** * Replacements to perform on <abbr>SQL</abbr> statements before execution. Some entries are the replacements @@ -211,7 +213,7 @@ public class SQLTranslator implements UnaryOperator<String> { * Other entries are replacements of missing columns by {@code CAST(NULL AS …)} expressions and replacements * of {@code BOOLEAN} comparator operands by {@code SMALLINT}. */ - private Map<String,String> replacements; + private Map<String, String> replacements; /** * The characters used for quoting identifiers, or an empty string if none. @@ -242,6 +244,15 @@ public class SQLTranslator implements UnaryOperator<String> { */ private boolean useBoolean; + /** + * Whether the database takes collation in account in {@code LIKE} clauses. + * Collations were working well in Derby, but the migration to HSQLDB forces + * us to create redundant conditions in the <abbr>SQL</abbr> statement without + * reaching the same power as Derby or PostgreSQL. + */ + @Workaround(library="HSQLDB", version="2.7.4") + final boolean supportsCollationInLike; + /** * Whether the sentinel table {@code "Coordinate_Operation"} or a variant has been found. * If {@code false}, then {@link EPSGInstaller} needs to be run. @@ -287,6 +298,7 @@ public class SQLTranslator implements UnaryOperator<String> { * @throws SQLException if an error occurred while querying the database metadata. */ public SQLTranslator(final DatabaseMetaData md, final String catalog, final String schema) throws SQLException { + supportsCollationInLike = Dialect.guess(md).supportsCollationInLike(); identifierQuote = md.getIdentifierQuoteString().trim(); wildcardEscape = md.getSearchStringEscape(); this.catalog = catalog; diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/package-info.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/package-info.java index 7a366ea7a1..9c3be9e7c9 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/package-info.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/package-info.java @@ -83,7 +83,7 @@ * @author Jody Garnett (Refractions) * @author Didier Richard (IGN) * @author John Grange - * @version 1.7 + * @version 1.8 * * @see org.apache.sis.metadata.sql * diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK26.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK26.java new file mode 100644 index 0000000000..c2f442f8be --- /dev/null +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK26.java @@ -0,0 +1,41 @@ +/* + * 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.pending.jdk; + + +/** + * Place holder for some functionalities defined in a JDK more recent than Java 11. + * + * @author Martin Desruisseaux (Geomatys) + */ +public final class JDK26 { + /** + * Do not allow instantiation of this class. + */ + private JDK26() { + } + + /** + * Placeholder for {@code Connection.enquoteLiteral(String)}. + * + * @param value the string to encode. + * @return the quoted value. + */ + public static String enquoteLiteral(String value) { + return '\'' + value.replace("'", "''") + '\''; + } +}
