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 ad0646c8a4 More type-safe way to specify the EPSG tables on which 
queries are executed.
ad0646c8a4 is described below

commit ad0646c8a4ee5a958a9ee8bb11c4d6395fc8fefc
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Aug 28 15:57:32 2025 +0200

    More type-safe way to specify the EPSG tables on which queries are executed.
---
 .../factory/MultiAuthoritiesFactory.java           |  10 +-
 .../referencing/factory/sql/AuthorityCodes.java    |  40 +--
 .../referencing/factory/sql/EPSGCodeFinder.java    | 109 ++++----
 .../referencing/factory/sql/EPSGDataAccess.java    | 257 +++++++++---------
 .../sis/referencing/factory/sql/TableInfo.java     | 299 ++++++++++++---------
 .../sis/referencing/factory/sql/TableInfoTest.java |  14 +-
 .../apache/sis/storage/netcdf/base/AxisType.java   |   2 +-
 7 files changed, 397 insertions(+), 334 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
index 15708c50f8..9e819db570 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
@@ -1511,7 +1511,9 @@ public class MultiAuthoritiesFactory extends 
GeodeticAuthorityFactory implements
      * but instead stores information for later execution.
      */
     private static final class Deferred extends 
AuthorityFactoryProxy<CoordinateOperationAuthorityFactory> {
-        Deferred() {super(CoordinateOperationAuthorityFactory.class, 
AuthorityFactoryIdentifier.Type.OPERATION);}
+        Deferred() {
+            super(CoordinateOperationAuthorityFactory.class, 
AuthorityFactoryIdentifier.Type.OPERATION);
+        }
 
         /** The authority code saved by the {@code createFromAPI(…)} method. */
         String code;
@@ -1791,8 +1793,8 @@ public class MultiAuthoritiesFactory extends 
GeodeticAuthorityFactory implements
         @Override
         final Set<IdentifiedObject> createFromCodes(final IdentifiedObject 
object) throws FactoryException {
             if (finders == null) try {
-                final ArrayList<IdentifiedObjectFinder> list = new 
ArrayList<>();
-                final Map<AuthorityFactory,Boolean> unique = new 
IdentityHashMap<>();
+                final var list = new ArrayList<IdentifiedObjectFinder>();
+                final var unique = new 
IdentityHashMap<AuthorityFactory,Boolean>();
                 final Iterator<AuthorityFactory> it = 
((MultiAuthoritiesFactory) factory).getAllFactories();
                 while (it.hasNext()) {
                     final AuthorityFactory candidate = it.next();
@@ -1809,7 +1811,7 @@ public class MultiAuthoritiesFactory extends 
GeodeticAuthorityFactory implements
             } catch (BackingStoreException e) {
                 throw e.unwrapOrRethrow(FactoryException.class);
             }
-            final Set<IdentifiedObject> found = new LinkedHashSet<>();
+            final var found = new LinkedHashSet<IdentifiedObject>();
             for (final IdentifiedObjectFinder finder : finders) {
                 found.addAll(finder.find(object));
             }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
index 2b26f07d70..afa766de0e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
@@ -66,6 +66,11 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
      */
     private static final int ALL_CODES = 0, NAME_FOR_CODE = 1, CODES_FOR_NAME 
= 2;
 
+    /**
+     * Number of queries stored in the {@link #sql} and {@link #statements} 
arrays.
+     */
+    private static final int NUM_QUERIES = 3;
+
     /**
      * The factory which is the owner of this map. One purpose of this field 
is to prevent
      * garbage collection of that factory as long as this map is in use. This 
is required
@@ -125,9 +130,8 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
      */
     AuthorityCodes(final TableInfo table, final Class<?> type, final 
EPSGDataAccess factory) throws SQLException {
         this.factory = factory;
-        final int count = (table.nameColumn != null) ? 3 : 1;
-        sql = new String[count];
-        statements = new Statement[count];
+        sql = new String[NUM_QUERIES];
+        statements = new Statement[NUM_QUERIES];
         /*
          * Build the SQL query for fetching the codes of all object. It is of 
the form:
          *
@@ -150,7 +154,7 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
          *
          *     SELECT code FROM table WHERE name LIKE ? AND DEPRECATED=FALSE 
ORDER BY code;
          */
-        if (count > CODES_FOR_NAME) {
+        if (NUM_QUERIES > CODES_FOR_NAME) {
             sql[CODES_FOR_NAME] = buffer.insert(conditionStart, 
table.nameColumn + " LIKE ? AND ").toString();
             /*
              * Workaround for Derby bug. See 
`SQLUtilities.filterFalsePositive(…)`.
@@ -165,12 +169,12 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
          *
          *     SELECT name FROM table WHERE code = ?
          */
-        if (count > NAME_FOR_CODE) {
+        if (NUM_QUERIES > NAME_FOR_CODE) {
             buffer.setLength(conditionStart);
             buffer.replace(columnNameStart, columnNameEnd, table.nameColumn);
             sql[NAME_FOR_CODE] = buffer.append(table.codeColumn).append(" = 
?").toString();
         }
-        for (int i=0; i<count; i++) {
+        for (int i=0; i<NUM_QUERIES; i++) {
             sql[i] = factory.translator.apply(sql[i]);
         }
     }
@@ -201,21 +205,19 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
      * Puts codes associated to the given name in the given collection.
      *
      * @param  pattern  the {@code LIKE} pattern of the name to search.
-     * @param  name     the original name (workaround for Derby bug).
+     * @param  name     the original name. This is a temporary workaround for 
a Derby bug (see {@code filterFalsePositive(…)}).
      * @param  addTo    the collection where to add the codes.
      * @throws SQLException if an error occurred while querying the database.
      */
     final void findCodesFromName(final String pattern, final String name, 
final Collection<Integer> addTo) throws SQLException {
-        if (statements.length > CODES_FOR_NAME) {
-            synchronized (factory) {
-                final PreparedStatement statement = 
prepareStatement(CODES_FOR_NAME);
-                statement.setString(1, pattern);
-                try (ResultSet result = statement.executeQuery()) {
-                    while (result.next()) {
-                        final int code = result.getInt(1);
-                        if (!result.wasNull() && 
SQLUtilities.filterFalsePositive(name, result.getString(2))) {
-                            addTo.add(code);
-                        }
+        synchronized (factory) {
+            final PreparedStatement statement = 
prepareStatement(CODES_FOR_NAME);
+            statement.setString(1, pattern);
+            try (ResultSet result = statement.executeQuery()) {
+                while (result.next()) {
+                    final int code = result.getInt(1);
+                    if (!result.wasNull() && 
SQLUtilities.filterFalsePositive(name, result.getString(2))) {
+                        addTo.add(code);
                     }
                 }
             }
@@ -317,7 +319,7 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
      */
     @Override
     public String get(final Object code) {
-        if (code != null && statements.length > NAME_FOR_CODE) {
+        if (code != null) {
             final int n;
             if (code instanceof Number) {
                 n = ((Number) code).intValue();
@@ -394,7 +396,7 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
         String size = null;
         synchronized (factory) {
             if (codes != null) {
-                size = "size " + (results != null ? "≥ " : "= ") + 
codes.size();
+                size = "size" + (results != null ? " ≥ " : " = ") + 
codes.size();
             }
         }
         return Strings.toString(getClass(), "type", type.getSimpleName(), 
null, size);
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 0c0df29970..2b51586929 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
@@ -47,6 +47,7 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.CollectionsExt;
 import org.apache.sis.util.collection.BackingStoreException;
@@ -509,7 +510,7 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
                     }
                 }
             }
-            dao.sort(source.table, addTo, 
Integer::intValue).ifPresent((sorted) -> {
+            dao.sort(source, addTo, Integer::intValue).ifPresent((sorted) -> {
                 addTo.clear();
                 addTo.addAll(JDK16.toList(sorted.mapToObj(Integer::valueOf)));
             });
@@ -592,9 +593,9 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
      */
     @Override
     protected Iterable<String> getCodeCandidates(final IdentifiedObject 
object) throws FactoryException {
-        for (final TableInfo table : TableInfo.EPSG) {
-            if (table.type.isInstance(object)) try {
-                return new CodeCandidates(object, table);
+        for (final TableInfo source : TableInfo.values()) {
+            if (source.isCandidate() && source.type.isInstance(object)) try {
+                return new CodeCandidates(object, source);
             } catch (SQLException exception) {
                 throw databaseFailure(exception);
             }
@@ -610,22 +611,25 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
         /** The object to search. */
         private final IdentifiedObject object;
 
+        /** Workaround for a Derby bug (see {@code filterFalsePositive(…)}). */
+        private String name;
+
+        /** {@code LIKE} Pattern of the name of the object to search. */
+        private String namePattern;
+
         /** Information about the tables of the object to search. */
         private final TableInfo source;
 
         /** Cache of codes found so far. */
         private final Set<Integer> codes;
 
-        /** Whether to include the codes of all objects, even the deprecated 
ones. */
-        private boolean includeAll;
+        /** Snapshot of the search domain as it was at collection construction 
time. */
+        private final Domain domain;
 
-        /** Whether to use only identifiers, name and aliases in the search. */
-        private boolean easySearch;
+        /** Whether to try to easy search methods before the expansive method. 
*/
+        private final boolean optimize;
 
-        /**
-         * Algorithm used for filling the {@link #codes} collection so far.
-         * 0 = identifiers, 1 = name, 2 = aliases, 3 = search based on 
properties.
-         */
+        /** Sequential number of the algorithm used for filling the {@link 
#codes} collection so far. */
         private byte searchMethod;
 
         /**
@@ -640,22 +644,16 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
         CodeCandidates(final IdentifiedObject object, final TableInfo source) 
throws SQLException, FactoryException {
             this.object = object;
             this.source = source;
+            this.domain = getSearchDomain();
             this.codes  = new LinkedHashSet<>();
-            switch (getSearchDomain()) {
-                case DECLARATION: easySearch = true; break;
-                case ALL_DATASET: includeAll = true; break;
-                case EXHAUSTIVE_VALID_DATASET: {
-                    // Skip the search methods based on identifiers, name or 
aliases.
-                    searchCodesFromProperties(object, false, codes);
-                    searchMethod = 3;
-                    return;
-                }
-            }
-            for (final Identifier id : object.getIdentifiers()) {
-                if (Constants.EPSG.equalsIgnoreCase(id.getCodeSpace())) try {
-                    codes.add(Integer.valueOf(id.getCode()));
-                } catch (NumberFormatException exception) {
-                    Logging.ignorableException(EPSGDataAccess.LOGGER, 
IdentifiedObjectFinder.class, "find", exception);
+            optimize = (domain != Domain.EXHAUSTIVE_VALID_DATASET);
+            if (optimize) {
+                for (final Identifier id : object.getIdentifiers()) {
+                    if (Constants.EPSG.equalsIgnoreCase(id.getCodeSpace())) 
try {
+                        codes.add(Integer.valueOf(id.getCode()));
+                    } catch (NumberFormatException exception) {
+                        Logging.ignorableException(EPSGDataAccess.LOGGER, 
IdentifiedObjectFinder.class, "find", exception);
+                    }
                 }
             }
             if (codes.isEmpty()) {
@@ -675,33 +673,39 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
         private boolean fetchMoreCodes(final Collection<Integer> addTo) throws 
SQLException, FactoryException {
             do {
                 switch (searchMethod) {
-                    case 0: findCodesFromName(false, addTo); break;     // 
Fetch codes for the name.
-                    case 1: findCodesFromName(true,  addTo); break;     // 
Fetch codes for the aliases.
-                    case 2: if (easySearch) break;                      // 
Search codes based on object properties.
-                            searchCodesFromProperties(object, includeAll, 
addTo);
-                            break;
-                    default: return false;
+                    case 0: {   // Fetch codes from the name.
+                        if (optimize) {
+                            name = getName(object);
+                            if (name != null) {     // Should never be null, 
but we are paranoiac.
+                                namePattern = dao.toLikePattern(name);
+                                dao.findCodesFromName(source, 
object.getClass(), namePattern, name, addTo);
+                            }
+                        }
+                        break;
+                    }
+                    case 1: {   // Fetch codes from the aliases.
+                        if (optimize) {
+                            if (namePattern != null) {
+                                dao.findCodesFromAlias(source, namePattern, 
name, addTo);
+                            }
+                        }
+                        break;
+                    }
+                    case 2: {   // Search codes based on object properties.
+                        if (domain != Domain.DECLARATION) {
+                            searchCodesFromProperties(object, domain == 
Domain.ALL_DATASET, addTo);
+                        }
+                        break;
+                    }
+                    default: {
+                        return false;
+                    }
                 }
                 searchMethod++;
             } while (addTo.isEmpty());
             return true;
         }
 
-        /**
-         * Adds the authority codes of all objects of the given name.
-         * Callers should update the {@link #searchMethod} flag accordingly.
-         *
-         * @param  alias  whether to search in the alias table rather than the 
main name.
-         * @param  codes  the collection where to add the codes that have been 
found.
-         * @throws SQLException if an error occurred while querying the 
database.
-         */
-        private void findCodesFromName(final boolean alias, final 
Collection<Integer> addTo) throws SQLException {
-            final String name = getName(object);
-            if (name != null) {     // Should never be null, but we are 
paranoiac.
-                dao.findCodesFromName(source.table, object.getClass(), name, 
alias, addTo);
-            }
-        }
-
         /**
          * Returns additional code candidates which were not yet returned by 
the iteration.
          * This method uses the next search method which hasn't be tried.
@@ -758,5 +762,14 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
                 }
             };
         }
+
+        /**
+         * Returns a string representation for debugging purposes.
+         * The {@code "size"} property may change during the iteration.
+         */
+        @Override
+        public String toString() {
+            return Strings.toString(getClass(), "object", getName(object), 
"source", source, "domain", domain, "size", codes.size());
+        }
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index 5158226b41..d39afd31b2 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -259,7 +259,7 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * </ol>
      *
      * Since we are not using the shared cache, there is a possibility that 
many objects are created for the same code.
-     * However, this duplication should not happen often. For example, each 
conventional <abbr>RS</abbr> should appears
+     * However, this duplication should not happen often. For example, each 
conventional <abbr>RS</abbr> should appear
      * in only one datum ensemble created by {@link 
#createDatumEnsemble(Integer, Map)}.
      *
      * <p>Keys are {@link Long} except the keys for naming systems which are 
{@link String}.</p>
@@ -267,7 +267,7 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * @see #getAxisName(Integer)
      * @see #getRealizationMethod(Integer)
      * @see #createConventionalRS(Integer)
-     * @see #createProperties(String, Integer, String, CharSequence, String, 
String, CharSequence, boolean)
+     * @see #createProperties(TableInfo, Integer, String, CharSequence, 
String, String, CharSequence, boolean)
      */
     private final Map<Object, Object> localCache = new HashMap<>();
 
@@ -600,8 +600,8 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
             }
         }
         if (source == null) {
-            for (TableInfo c : TableInfo.EPSG) {
-                if (c.type.isAssignableFrom(type)) {
+            for (TableInfo c : TableInfo.values()) {
+                if (c.isCandidate() && c.type.isAssignableFrom(type)) {
                     if (source != null) {
                         return null;        // The specified type is too 
generic.
                     }
@@ -718,28 +718,29 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * Converts <abbr>EPSG</abbr> codes or <abbr>EPSG</abbr> names to the 
numerical identifiers (the primary keys).
      * This method can be seen as the converse of above {@link 
#getDescriptionText(Class, String)} method.
      *
-     * @param  table  the table where the code should appear, or {@code null} 
for no search by name.
-     * @param  codes  the codes or names to convert to primary keys, as an 
array of length 1 or 2.
+     * @param  source  the table where the code should appear, or {@code null} 
for no search by name.
+     * @param  codes   the codes or names to convert to primary keys, as an 
array of length 1 or 2.
      * @return the numerical identifiers (i.e. the table primary key values).
      * @throws SQLException if an error occurred while querying the database.
      * @throws FactoryDataException if code is a name and two distinct 
numerical codes match the name.
      * @throws NoSuchAuthorityCodeException if code is a name and no numerical 
code match the name.
      */
-    private int[] toPrimaryKeys(final String table, final String... codes) 
throws SQLException, FactoryException {
+    private int[] toPrimaryKeys(final TableInfo source, final String... codes) 
throws SQLException, FactoryException {
         final int[] primaryKeys = new int[codes.length];
         for (int i=0; i<codes.length; i++) {
             String code = codes[i];
-            if (table != null && !isPrimaryKey(code)) {
+            if (source != null && !isPrimaryKey(code)) {
                 /*
                  * The given string is not a numerical code. Search the value 
in the database.
                  * We search first in the table of the query. If the name is 
not found there,
                  * then we will search in the aliases table as a fallback.
                  */
                 final var result = new ArrayList<Integer>();
-                findCodesFromName(table, null, code, false, result);
+                final String pattern = toLikePattern(code);
+                findCodesFromName(source, source.type, pattern, code, result);
                 if (result.isEmpty()) {
                     // Search in aliases only if no match was found in primary 
names.
-                    findCodesFromName(table, null, code, true, result);
+                    findCodesFromAlias(source, pattern, code, result);
                 }
                 Integer resolved = null;
                 for (Integer value : result) {
@@ -767,43 +768,57 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
         return primaryKeys;
     }
 
+    /**
+     * Returns the given object name as a pattern which can be used in a 
{@code LIKE} clause.
+     * This method does not change the character case for avoiding the need to 
use {@code LOWER}
+     * in the <abbr>SQL</abbr> statement (because it may prevent the use of 
the database index).
+     */
+    final String toLikePattern(final String name) {
+        return SQLUtilities.toLikePattern(name, false, 
translator.wildcardEscape);
+    }
+
     /**
      * Finds the authority codes for the given name.
      *
-     * @param  table  the table where the code should appear.
-     * @parma  type   the type of object to searh, or {@code null} for 
inferring from the table.
-     * @param  name   the name to search.
-     * @param  alias  whether to search in the alias table rather than the 
main name.
-     * @param  addTo  the collection where to add the codes that have been 
found.
+     * @param  source   information about the table where the code should 
appear.
+     * @param  type     the type of object to search. Should be assignable to 
{@code source.type}.
+     * @param  pattern  the name to search as a pattern that can be used with 
{@code LIKE}.
+     * @param  name     the original name. This is a temporary workaround for 
a Derby bug (see {@code filterFalsePositive(…)}).
+     * @param  addTo    the collection where to add the codes that have been 
found.
      * @throws SQLException if an error occurred while querying the database.
      */
-    final void findCodesFromName(final String table, final Class<?> type, 
final String name, final boolean alias, final Collection<Integer> addTo)
+    final void findCodesFromName(final TableInfo source, final Class<?> type, 
final String pattern, final String name, final Collection<Integer> addTo)
             throws SQLException
     {
-        final String pattern = SQLUtilities.toLikePattern(name, false, 
translator.wildcardEscape);
-        if (alias) {
-            final PreparedStatement stmt = prepareStatement(
-                    "AliasKey",
-                    "SELECT OBJECT_CODE, ALIAS"
-                            + " FROM \"Alias\""
-                            + " WHERE OBJECT_TABLE_NAME=? AND ALIAS LIKE ?");
-            stmt.setString(1, translator.toActualTableName(table));
-            stmt.setString(2, pattern);
-            try (ResultSet result = stmt.executeQuery()) {
-                while (result.next()) {
-                    if (SQLUtilities.filterFalsePositive(name, 
result.getString(2))) {
-                        addTo.add(getOptionalInteger(result, 1));
-                    }
-                }
-            }
-        } else {
-            for (final TableInfo source : TableInfo.EPSG) {
-                if (table.equals(source.table)) {
-                    AuthorityCodes codes = getCodeMap(type == null ? 
source.type : type, source, false);
-                    if (codes != null) {
-                        codes.findCodesFromName(pattern, name, addTo);
-                        break;
-                    }
+        AuthorityCodes codes = getCodeMap(type, source, false);
+        if (codes != null) {
+            codes.findCodesFromName(pattern, name, addTo);
+        }
+    }
+
+    /**
+     * Finds the authority codes for the given alias.
+     *
+     * @param  source   information about the table where the code should 
appear.
+     * @param  pattern  the name to search as a pattern that can be used with 
{@code LIKE}.
+     * @param  name     the original name. This is a temporary workaround for 
a Derby bug (see {@code filterFalsePositive(…)}).
+     * @param  addTo    the collection where to add the codes that have been 
found.
+     * @throws SQLException if an error occurred while querying the database.
+     */
+    final void findCodesFromAlias(final TableInfo source, final String 
pattern, final String name, final Collection<Integer> addTo)
+            throws SQLException
+    {
+        final PreparedStatement stmt = prepareStatement(
+                "AliasKey",
+                "SELECT OBJECT_CODE, ALIAS"
+                        + " FROM \"Alias\""
+                        + " WHERE OBJECT_TABLE_NAME=? AND ALIAS LIKE ?");
+        stmt.setString(1, translator.toActualTableName(source.table));
+        stmt.setString(2, pattern);
+        try (ResultSet result = stmt.executeQuery()) {
+            while (result.next()) {
+                if (SQLUtilities.filterFalsePositive(name, 
result.getString(2))) {
+                    addTo.add(getOptionalInteger(result, 1));
                 }
             }
         }
@@ -829,29 +844,27 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      *       in their {@code finally} block.</li>
      * </ul>
      *
-     * @param  table  the table where the code should appears.
-     * @param  sql    the SQL statement to use for creating the {@link 
PreparedStatement} object.
-     *                Will be used only if no prepared statement was already 
created for the given code.
-     * @param  codes  the codes of the object to create, as an array of length 
1 or 2.
+     * @param  source  information about the table where the code should 
appear.
+     * @param  sql     the <abbr>SQL</abbr> statement to use for creating the 
{@link PreparedStatement} object.
+     *                 Will be used only if no prepared statement was already 
created for the given code.
+     * @param  codes   the codes of the object to create, as an array of 
length 1 or 2.
      * @return the result of the query.
      * @throws SQLException if an error occurred while querying the database.
      */
-    private ResultSet executeSingletonQuery(final String table, final String 
sql, final String... codes)
+    private ResultSet executeSingletonQuery(final TableInfo source, final 
String sql, final String... codes)
             throws SQLException, FactoryException
     {
         assert Thread.holdsLock(this);
-        assert sql.contains('"' + table + '"') : table;
-    //  assert (codeColumn == null) || sql.contains(codeColumn) || 
table.equals("Extent") : codeColumn;
-    //  assert (nameColumn == null) || sql.contains(nameColumn) || 
table.equals("Extent") : nameColumn;
-        final int[] keys = toPrimaryKeys(table, codes);
-        currentSingletonQuery = new QueryID(table, keys, 
currentSingletonQuery);
+        assert source.validate(sql) : source;
+        final int[] keys = toPrimaryKeys(source, codes);
+        currentSingletonQuery = new QueryID(source.table, keys, 
currentSingletonQuery);
         if (currentSingletonQuery.isAlreadyInProgress()) {
             throw new FactoryDataException(resources().getString(
                     Resources.Keys.RecursiveCreateCallForCode_2,
-                    TableInfo.getObjectClassName(table).orElse(table),
+                    source.type.getSimpleName(),
                     (codes.length == 1) ? codes[0] : Arrays.toString(codes)));
         }
-        return executeQueryForCodes(table, sql, keys);
+        return executeQueryForCodes(source.table, sql, keys);
     }
 
     /**
@@ -1184,11 +1197,11 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
     /**
      * Logs a warning saying that the given code is deprecated and returns the 
code of the proposed replacement.
      *
-     * @param  table   the table of the deprecated code.
+     * @param  source  information about the table where the deprecated object 
is found.
      * @param  code    the deprecated code.
      * @return the proposed replacement (may be the "(none)" text). Never 
empty.
      */
-    private String getReplacement(final String table, final Integer code, 
final Locale locale) throws SQLException {
+    private String getReplacement(final TableInfo source, final Integer code, 
final Locale locale) throws SQLException {
         String reason = null;
         String replacedBy;
 search: try (ResultSet result = executeMetadataQuery("Deprecation",
@@ -1196,7 +1209,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                         + " FROM \"Deprecation\""
                         + " WHERE OBJECT_TABLE_NAME=?"
                         + " AND OBJECT_CODE=?",
-                translator.toActualTableName(table), code))
+                translator.toActualTableName(source.table), code))
         {
             while (result.next()) {
                 reason    = getOptionalString (result, 1);
@@ -1216,7 +1229,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         if (!quiet) {
             Logging.completeAndLog(LOGGER,
                     EPSGDataAccess.class,
-                    
"create".concat(TableInfo.getObjectClassName(table).orElse("")),
+                    "create".concat(source.type.getSimpleName()),
                     Resources.forLocale(locale).createLogRecord(
                             Level.WARNING,
                             Resources.Keys.DeprecatedCode_3,
@@ -1244,7 +1257,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
      *     }
      *     }
      *
-     * @param  table       the table on which a query has been executed.
+     * @param  source      information about the table on which a query has 
been executed.
      * @param  code        the EPSG code of the object to construct.
      * @param  name        the name for the {@link IdentifiedObject} to 
construct.
      * @param  description a description associated with the name, or {@code 
null} if none.
@@ -1255,7 +1268,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
      * @return the name together with a set of properties.
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    private Map<String,Object> createProperties(final String       table,
+    private Map<String,Object> createProperties(final TableInfo    source,
                                                 final Integer      code,
                                                       String       name,       
 // May be replaced by an alias.
                                                 final CharSequence description,
@@ -1270,7 +1283,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
          * se we need to fetch and store the extent before to populate the 
`properties` map.
          */
         final Extent extent = (extentCode == null) ? null : 
createExtent(extentCode);
-        final String actualTable = translator.toActualTableName(table);
+        final String actualTable = translator.toActualTableName(source.table);
         /*
          * Get all domains for the object identified by the given code.
          * The table used nere is new in version 10 of EPSG database.
@@ -1359,7 +1372,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final ImmutableIdentifier identifier;
         if (deprecated) {
             properties.put(AbstractIdentifiedObject.DEPRECATED_KEY, 
Boolean.TRUE);
-            final String replacedBy = getReplacement(table, code, locale);
+            final String replacedBy = getReplacement(source, code, locale);
             identifier = new DeprecatedCode(
                     authority,
                     Constants.EPSG,
@@ -1419,21 +1432,20 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final boolean isPrimaryKey = isPrimaryKey(code);
         final var query = new StringBuilder("SELECT ");
         final int queryStart = query.length();
-        int found = -1;
+        TableInfo found = null;
         try {
             final int key = isPrimaryKey ? toPrimaryKeys(null, code)[0] : 0;
-            for (int i = 0; i < TableInfo.EPSG.length; i++) {
-                final TableInfo table = TableInfo.EPSG[i];
-                final String column = isPrimaryKey ? table.codeColumn : 
table.nameColumn;
-                if (column == null) {
+            for (final TableInfo source : TableInfo.values()) {
+                if (!(source.isCandidate() && 
IdentifiedObject.class.isAssignableFrom(source.type))) {
                     continue;
                 }
+                final String column = isPrimaryKey ? source.codeColumn : 
source.nameColumn;
                 query.setLength(queryStart);
-                query.append(table.codeColumn);
+                query.append(source.codeColumn);
                 if (!isPrimaryKey) {
                     query.append(", ").append(column);      // Only for 
filterFalsePositive(…).
                 }
-                query.append(" FROM ").append(table.fromClause)
+                query.append(" FROM ").append(source.fromClause)
                      .append(" WHERE ").append(column).append(isPrimaryKey ? " 
= ?" : " LIKE ?");
                 try (PreparedStatement stmt = 
connection.prepareStatement(translator.apply(query.toString()))) {
                     /*
@@ -1443,7 +1455,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                     if (isPrimaryKey) {
                         stmt.setInt(1, key);
                     } else {
-                        stmt.setString(1, SQLUtilities.toLikePattern(code, 
false, translator.wildcardEscape));
+                        stmt.setString(1, toLikePattern(code));
                     }
                     Integer present = null;
                     try (ResultSet result = stmt.executeQuery()) {
@@ -1454,10 +1466,10 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                         }
                     }
                     if (present != null) {
-                        if (found >= 0) {
+                        if (found != null) {
                             throw new 
FactoryDataException(error().getString(Errors.Keys.DuplicatedIdentifier_1, 
code));
                         }
-                        found = i;
+                        found = source;
                     }
                 }
             }
@@ -1467,18 +1479,18 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         /*
          * If a record has been found in one table, then delegates to the 
appropriate method.
          */
-        if (found >= 0) {
+        if (found != null) {
             switch (found) {
-                case 0:  return createCoordinateReferenceSystem(code);
-                case 1:  return createCoordinateSystem         (code);
-                case 2:  return createCoordinateSystemAxis     (code);
-                case 3:  return createDatum                    (code);
-                case 4:  return createEllipsoid                (code);
-                case 5:  return createPrimeMeridian            (code);
-                case 6:  return createCoordinateOperation      (code);
-                case 7:  return createOperationMethod          (code);
-                case 8:  return createParameterDescriptor      (code);
-                case 9:  break; // Cannot cast Unit to IdentifiedObject
+                case CRS:            return 
createCoordinateReferenceSystem(code);
+                case CS:             return createCoordinateSystem         
(code);
+                case AXIS:           return createCoordinateSystemAxis     
(code);
+                case DATUM:          return createDatum                    
(code);
+                case ELLIPSOID:      return createEllipsoid                
(code);
+                case PRIME_MERIDIAN: return createPrimeMeridian            
(code);
+                case OPERATION:      return createCoordinateOperation      
(code);
+                case METHOD:         return createOperationMethod          
(code);
+                case PARAMETER:      return createParameterDescriptor      
(code);
+                case UNIT:           break; // Cannot cast Unit to 
IdentifiedObject
                 default: throw new AssertionError(found);                   // 
Should not happen
             }
         }
@@ -1562,7 +1574,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         CoordinateReferenceSystem returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Coordinate Reference System",
+                TableInfo.CRS,
                 "SELECT"+ /* column  1 */ " COORD_REF_SYS_CODE,"
                         + /* column  2 */ " COORD_REF_SYS_NAME,"
                         + /* column  3 */ " AREA_OF_USE_CODE,"      // 
Deprecated since EPSG version 10 (always NULL)
@@ -1806,7 +1818,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                  */
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Map<String,Object> properties = createProperties(
-                        "Coordinate Reference System", epsg, name, null, area, 
scope, remarks, deprecated);
+                        TableInfo.CRS, epsg, name, null, area, scope, remarks, 
deprecated);
                 final CoordinateReferenceSystem crs = create(constructor, 
owner.crsFactory, properties);
                 returnValue = ensureSingleton(crs, returnValue, code);
                 if (result.isClosed()) break;   // See createProperties(…) for 
explanation.
@@ -1875,7 +1887,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         Datum returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Datum",
+                TableInfo.DATUM,
                 "SELECT"+ /* column  1 */ " DATUM_CODE,"
                         + /* column  2 */ " DATUM_NAME,"
                         + /* column  3 */ " DATUM_TYPE,"
@@ -1968,7 +1980,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                 final IdentifiedObject conventionalRS = 
createConventionalRS(convRSCode);
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Map<String,Object> properties = createProperties(
-                        "Datum", epsg, name, null, area, scope, remarks, 
deprecated);
+                        TableInfo.DATUM, epsg, name, null, area, scope, 
remarks, deprecated);
                 properties.put(Datum.ANCHOR_DEFINITION_KEY, anchor);
                 properties.put(Datum.ANCHOR_EPOCH_KEY,      epoch);
                 properties.put(Datum.PUBLICATION_DATE_KEY,  publish);
@@ -2102,7 +2114,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         var returnValue = (IdentifiedObject) localCache.get(cacheKey);
         if (returnValue == null) {
             try (ResultSet result = executeQueryForCodes(
-                    "Conventional RS",
+                    TableInfo.CONVENTIONAL_RS.table,
                     "SELECT"+ /* column 1 */ " CONVENTIONAL_RS_CODE,"
                             + /* column 2 */ " CONVENTIONAL_RS_NAME,"
                             + /* column 3 */ " REMARKS,"
@@ -2121,7 +2133,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                      */
                     @SuppressWarnings("LocalVariableHidesMemberVariable")
                     final Map<String,Object> properties = createProperties(
-                            "Conventional RS", epsg, name, null, null, null, 
remarks, deprecated);
+                            TableInfo.CONVENTIONAL_RS, epsg, name, null, null, 
null, remarks, deprecated);
                     returnValue = ensureSingleton(new 
AbstractIdentifiedObject(properties), returnValue, code);
                     if (result.isClosed()) break;   // See createProperties(…) 
for explanation.
                 }
@@ -2163,7 +2175,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         Ellipsoid returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Ellipsoid",
+                TableInfo.ELLIPSOID,
                 "SELECT"+ /* column 1 */ " ELLIPSOID_CODE,"
                         + /* column 2 */ " ELLIPSOID_NAME,"
                         + /* column 3 */ " SEMI_MAJOR_AXIS,"
@@ -2202,7 +2214,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                  */
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Map<String,Object> properties = createProperties(
-                        "Ellipsoid", epsg, name, null, null, null, remarks, 
deprecated);
+                        TableInfo.ELLIPSOID, epsg, name, null, null, null, 
remarks, deprecated);
                 final Ellipsoid ellipsoid;
                 if (useSemiMinor) {
                     // We only have semiMinorAxis defined. It is OK
@@ -2265,7 +2277,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         PrimeMeridian returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Prime Meridian",
+                TableInfo.PRIME_MERIDIAN,
                 "SELECT"+ /* column 1 */ " PRIME_MERIDIAN_CODE,"
                         + /* column 2 */ " PRIME_MERIDIAN_NAME,"
                         + /* column 3 */ " GREENWICH_LONGITUDE,"
@@ -2288,7 +2300,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                  * information needed from the `ResultSet`, because it may be 
closed.
                  */
                 final PrimeMeridian primeMeridian = 
owner.datumFactory.createPrimeMeridian(
-                        createProperties("Prime Meridian", epsg, name, null, 
null, null, remarks, deprecated),
+                        createProperties(TableInfo.PRIME_MERIDIAN, epsg, name, 
null, null, null, remarks, deprecated),
                         longitude, unit);
                 returnValue = ensureSingleton(primeMeridian, returnValue, 
code);
                 if (result.isClosed()) break;   // See createProperties(…) for 
explanation.
@@ -2344,7 +2356,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         final var deferred = new ArrayList<Map.Entry<DefaultVerticalExtent, 
Integer>>();
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Extent",
+                TableInfo.EXTENT,
                 "SELECT"+ /* column  1 */ " EXTENT_DESCRIPTION,"
                         + /* column  2 */ " BBOX_SOUTH_BOUND_LAT,"
                         + /* column  3 */ " BBOX_NORTH_BOUND_LAT,"
@@ -2461,7 +2473,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         CoordinateSystem returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Coordinate System",
+                TableInfo.CS,
                 "SELECT"+ /* column 1 */ " COORD_SYS_CODE,"
                         + /* column 2 */ " COORD_SYS_NAME,"
                         + /* column 3 */ " COORD_SYS_TYPE,"
@@ -2496,7 +2508,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                  */
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Map<String,Object> properties = createProperties(
-                        "Coordinate System", epsg, name, null, null, null, 
remarks, deprecated);
+                        TableInfo.CS, epsg, name, null, null, null, remarks, 
deprecated);
                 /*
                  * The following switch statement should have a case for all 
"CS Kind" values enumerated
                  * in the `Prepare.sql` file, except that the values in this 
Java code are in lower cases.
@@ -2622,7 +2634,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         CoordinateSystemAxis returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Coordinate Axis",
+                TableInfo.AXIS,
                 "SELECT"+ /* column 1 */ " COORD_AXIS_CODE,"
                         + /* column 2 */ " COORD_AXIS_NAME_CODE,"
                         + /* column 3 */ " COORD_AXIS_ORIENTATION,"
@@ -2649,7 +2661,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                  * information needed from the `ResultSet`, because it may be 
closed.
                  */
                 final CoordinateSystemAxis axis = 
owner.csFactory.createCoordinateSystemAxis(
-                        createProperties("Coordinate Axis", epsg, an.name, 
an.description, null, null, an.remarks, false),
+                        createProperties(TableInfo.AXIS, epsg, an.name, 
an.description, null, null, an.remarks, false),
                         abbreviation, direction, owner.createUnit(unit));
                 returnValue = ensureSingleton(axis, returnValue, code);
                 if (result.isClosed()) break;   // See createProperties(…) for 
explanation.
@@ -2762,7 +2774,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         Unit<?> returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Unit of Measure",
+                TableInfo.UNIT,
                 "SELECT"+ /* column 1 */ " UOM_CODE,"
                         + /* column 2 */ " FACTOR_B,"
                         + /* column 3 */ " FACTOR_C,"
@@ -2844,7 +2856,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         ParameterDescriptor<?> returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Coordinate_Operation Parameter",
+                TableInfo.PARAMETER,
                 "SELECT"+ /* column 1 */ " PARAMETER_CODE,"
                         + /* column 2 */ " PARAMETER_NAME,"
                         + /* column 3 */ " DESCRIPTION,"
@@ -2872,7 +2884,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                      */
                     type = Double.class;
                     try (ResultSet r = executeQueryForCodes(
-                            "ParameterType",
+                            "Parameter Type",
                             "SELECT PARAM_VALUE_FILE_REF"
                                     + " FROM \"Coordinate_Operation Parameter 
Value\""
                                     + " WHERE PARAM_VALUE_FILE_REF IS NOT NULL"
@@ -2896,7 +2908,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                      */
                     units = new LinkedHashSet<>();
                     try (ResultSet r = executeQueryForCodes(
-                            "ParameterUnit",
+                            "Parameter Unit",
                             "SELECT UOM_CODE"
                                     + " FROM \"Coordinate_Operation Parameter 
Value\""
                                     + " WHERE (PARAMETER_CODE = ?)"
@@ -2929,7 +2941,7 @@ next:                   while (r.next()) {
                  */
                 InternationalString isReversible = null;
                 try (ResultSet r = executeQueryForCodes(
-                        "ParameterSign",
+                        "Parameter Sign",
                         "SELECT DISTINCT PARAM_SIGN_REVERSAL"
                                 + " FROM \"Coordinate_Operation Parameter 
Usage\""
                                 + " WHERE (PARAMETER_CODE = ?)", epsg))
@@ -2964,7 +2976,7 @@ next:                   while (r.next()) {
                  */
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Map<String,Object> properties = createProperties(
-                        "Coordinate_Operation Parameter", epsg, name, null, 
null, null, isReversible, deprecated);
+                        TableInfo.PARAMETER, epsg, name, null, null, null, 
isReversible, deprecated);
                 properties.put(Identifier.DESCRIPTION_KEY, description);
                 final var descriptor = new 
DefaultParameterDescriptor<>(properties, 1, 1, type, valueDomain, null, null);
                 returnValue = ensureSingleton(descriptor, returnValue, code);
@@ -3094,7 +3106,7 @@ next:                   while (r.next()) {
         OperationMethod returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Coordinate_Operation Method",
+                TableInfo.METHOD,
                 "SELECT"+ /* column 1 */ " COORD_OP_METHOD_CODE,"
                         + /* column 2 */ " COORD_OP_METHOD_NAME,"
                         + /* column 3 */ " REMARKS,"
@@ -3121,7 +3133,7 @@ next:                   while (r.next()) {
                  */
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Map<String,Object> properties = createProperties(
-                        "Coordinate_Operation Method", epsg, name, null, null, 
null, remarks, deprecated);
+                        TableInfo.METHOD, epsg, name, null, null, null, 
remarks, deprecated);
                 /*
                  * Note: we do not store the formula at this time, because the 
text is very verbose and rarely used.
                  */
@@ -3170,7 +3182,7 @@ next:                   while (r.next()) {
         CoordinateOperation returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
-                "Coordinate_Operation",
+                TableInfo.OPERATION,
                 "SELECT"+ /* column  1 */ " COORD_OP_CODE,"
                         + /* column  2 */ " COORD_OP_NAME,"
                         + /* column  3 */ " COORD_OP_TYPE,"
@@ -3316,7 +3328,7 @@ next:                   while (r.next()) {
                  */
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Map<String,Object> properties = createProperties(
-                        "Coordinate_Operation", epsg, name, null, area, scope, 
remarks, deprecated);
+                        TableInfo.OPERATION, epsg, name, null, area, scope, 
remarks, deprecated);
                 properties.put(CoordinateOperations.OPERATION_TYPE_KEY, 
operationType);
                 properties.put(CoordinateOperations.PARAMETERS_KEY, 
parameterValues);
                 properties.put(CoordinateOperation .OPERATION_VERSION_KEY, 
version);
@@ -3399,7 +3411,7 @@ next:                   while (r.next()) {
              * and supersession tables.
              */
             final List<String> codes = Arrays.asList(set.getAuthorityCodes());
-            sort("Coordinate_Operation", codes, 
Integer::parseInt).ifPresent((sorted) -> {
+            sort(TableInfo.OPERATION, codes, 
Integer::parseInt).ifPresent((sorted) -> {
                 
set.setAuthorityCodes(sorted.mapToObj(Integer::toString).toArray(String[]::new));
             });
         } catch (SQLException exception) {
@@ -3442,19 +3454,19 @@ next:                   while (r.next()) {
      * Except for the codes moved as a result of pairwise ordering, this 
method tries to preserve the old
      * ordering of the supplied codes (since deprecated operations should 
already be last).
      *
-     * @param  table  the table of the objects for which to check for 
supersession.
-     * @param  codes  the codes to sort. This collection will not be modified 
by this method.
-     * @param  parser the method to invoke for converting a {@code codes} 
element to an integer.
+     * @param  source  the table of the objects for which to check for 
supersession.
+     * @param  codes   the codes to sort. This collection will not be modified 
by this method.
+     * @param  parser  the method to invoke for converting a {@code codes} 
element to an integer.
      * @return codes of sorted elements, or empty if this method did not 
change the codes order.
      */
-    final synchronized <C extends Comparable<?>> Optional<IntStream> 
sort(final String table, final Collection<C> codes, final ToIntFunction<C> 
parser)
+    final synchronized <C extends Comparable<?>> Optional<IntStream> 
sort(final TableInfo source, final Collection<C> codes, final ToIntFunction<C> 
parser)
             throws SQLException, FactoryException
     {
         final int size = codes.size();
         if (size > 1) try {
             final var elements = new ObjectPertinence[size];
             final var extents = new ArrayList<String>();
-            final String actualTable = translator.toActualTableName(table);
+            final String actualTable = 
translator.toActualTableName(source.table);
             int count = 0;
             for (final C code : codes) {
                 final int key;
@@ -3473,20 +3485,15 @@ next:                   while (r.next()) {
                      * Note: if this block is deleted, consider deleting also
                      * the finally block and the `TableInfo.areaOfUse` flag.
                      */
-                    for (final TableInfo info : TableInfo.EPSG) {
-                        if (table.equals(info.table)) {
-                            if (info.areaOfUse) {
-                                try (ResultSet result = executeQueryForCodes(
-                                        "Area",     // Table from EPSG version 
9. Does not exist anymore in version 10.
-                                        "SELECT AREA_OF_USE_CODE FROM \"" + 
table + "\" WHERE " + info.codeColumn + "=?",
-                                        key))
-                                {
-                                    while (result.next()) {
-                                        extents.add(getString(code, result, 
1));
-                                    }
-                                }
+                    if (source.areaOfUse) {
+                        try (ResultSet result = executeQueryForCodes(
+                                "Area",     // Table from EPSG version 9. Does 
not exist anymore in version 10.
+                                "SELECT AREA_OF_USE_CODE FROM \"" + 
source.table + "\" WHERE " + source.codeColumn + "=?",
+                                key))
+                        {
+                            while (result.next()) {
+                                extents.add(getString(code, result, 1));
                             }
-                            break;
                         }
                     }
                 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
index 5acf36cebb..aed9fa2b7c 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
@@ -16,8 +16,8 @@
  */
 package org.apache.sis.referencing.factory.sql;
 
-import java.util.Optional;
 import javax.measure.Unit;
+import org.opengis.metadata.extent.Extent;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.cs.*;
 import org.opengis.referencing.crs.*;
@@ -25,143 +25,172 @@ import org.opengis.referencing.datum.*;
 import org.opengis.referencing.operation.*;
 import org.opengis.parameter.ParameterDescriptor;
 import org.apache.sis.referencing.privy.WKTKeywords;
-import org.apache.sis.util.CharSequences;
 
 // Specific to the geoapi-4.0 branch:
 import org.apache.sis.referencing.crs.DefaultGeocentricCRS;
 
 
 /**
- * Information about a specific table. This class uses the mixed-case variant 
of the <abbr>EPSG</abbr> table names.
- * If needed, those names will be converted by {@link 
SQLTranslator#apply(String)} to the actual table names.
+ * Information (such as columns of particular interest) about a specific 
<abbr>EPSG</abbr> table.
+ * This class uses the mixed-case variant of the <abbr>EPSG</abbr> {@linkplain 
#table table names}.
+ * The {@link #values()} can be tested in preference order for finding the 
table of an object.
+ * Those tables are used by the {@link EPSGDataAccess#createObject(String)} 
method in order to
+ * detect which of the following methods should be invoked for a given code:
+ *
+ * {@link EPSGDataAccess#createCoordinateReferenceSystem(String)}
+ * {@link EPSGDataAccess#createCoordinateSystem(String)}
+ * {@link EPSGDataAccess#createDatum(String)}
+ * {@link EPSGDataAccess#createEllipsoid(String)}
+ * {@link EPSGDataAccess#createUnit(String)}
+ *
+ * <h4>Ambiguity</h4>
+ * As of ISO 19111:2019, we have no standard way to identify the geocentric 
case from a {@link Class} argument
+ * because the standard does not provide the {@code GeocentricCRS} interface. 
This implementation fallbacks on
+ * the <abbr>SIS</abbr>-specific geocentric <abbr>CRS</abbr> class. This 
special case is implemented in the
+ * {@link #where(EPSGDataAccess, IdentifiedObject, StringBuilder)} method.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  */
-final class TableInfo {
+enum TableInfo {
     /**
-     * The item used for coordinate reference systems.
+     * Information about the "Coordinate Reference System" table.
      */
-    static final TableInfo CRS;
+    CRS(CoordinateReferenceSystem.class,
+            "\"Coordinate Reference System\"",
+            "COORD_REF_SYS_CODE",
+            "COORD_REF_SYS_NAME",
+            "COORD_REF_SYS_KIND",
+            new Class<?>[] { ProjectedCRS.class,   GeographicCRS.class,   
DefaultGeocentricCRS.class,
+                             VerticalCRS.class,    CompoundCRS.class,     
EngineeringCRS.class,
+                             DerivedCRS.class,     TemporalCRS.class,     
ParametricCRS.class},     // See comment below
+            new String[]   {"projected",          "geographic",          
"geocentric",
+                            "vertical",           "compound",            
"engineering",
+                            "derived",            "temporal",            
"parametric"},             // See comment below
+            "SHOW_CRS", true),
+            /*
+             * Above declaration could omit Derived, Temporal and Parametric 
cases because they are not defined
+             * by the EPSG repository (at least as of version 8.9). In 
particular we are not sure if EPSG would
+             * chose to use "time" or "temporal". However, omitting those 
types slow down a lot the search for
+             * CRS matching an existing one (even if it still work).
+             */
 
     /**
-     * The item used for datums.
+     * Information about the "Datum" table.
      */
-    static final TableInfo DATUM;
+    DATUM(Datum.class,
+            "\"Datum\"",
+            "DATUM_CODE",
+            "DATUM_NAME",
+            "DATUM_TYPE",
+            new Class<?>[] { GeodeticDatum.class,  VerticalDatum.class,   
EngineeringDatum.class,
+                             TemporalDatum.class,  ParametricDatum.class},
+            new String[]   {"geodetic",           "vertical",            
"engineering",
+                            "temporal",           "parametric"},         // 
Same comment as in the CRS case above.
+            null, true),
 
     /**
-     * The item used for ellipsoids.
+     * Information about the "Conventional RS" table.
+     * This enumeration usually needs to be ignored because the current type 
is too generic.
+     *
+     * @see #isCandidate()
      */
-    static final TableInfo ELLIPSOID;
+    CONVENTIONAL_RS(IdentifiedObject.class,
+            "\"Conventional RS\"",
+            "CONVENTIONAL_RS_CODE",
+            "CONVENTIONAL_RS_NAME",
+            null, null, null, null, false),
 
     /**
-     * List of tables and columns to test for existence of codes values.
-     * Those tables are used by the {@link 
EPSGDataAccess#createObject(String)} method
-     * in order to detect which of the following methods should be invoked for 
a given code:
-     *
-     * {@link EPSGDataAccess#createCoordinateReferenceSystem(String)}
-     * {@link EPSGDataAccess#createCoordinateSystem(String)}
-     * {@link EPSGDataAccess#createDatum(String)}
-     * {@link EPSGDataAccess#createEllipsoid(String)}
-     * {@link EPSGDataAccess#createUnit(String)}
-     *
-     * The order is significant: it is the key for a {@code switch} statement.
-     *
-     * <h4>Ambiguity</h4>
-     * As of ISO 19111:2019, we have no standard way to identify the 
geocentric case from a {@link Class} argument
-     * because the standard does not provide the {@code GeocentricCRS} 
interface. This implementation fallbacks on
-     * the SIS-specific geocentric CRS class, with a {@link 
#where(EPSGDataAccess, IdentifiedObject, StringBuilder)}
-     * method which will substitute implementation-neutral objects by the 
Apache SIS class.
+     * Information about the "Ellipsoid" table.
      */
-    static final TableInfo[] EPSG = {
-        CRS = new TableInfo(CoordinateReferenceSystem.class,
-                "\"Coordinate Reference System\"",
-                "COORD_REF_SYS_CODE",
-                "COORD_REF_SYS_NAME",
-                "COORD_REF_SYS_KIND",
-                new Class<?>[] { ProjectedCRS.class,   GeographicCRS.class,   
DefaultGeocentricCRS.class,
-                                 VerticalCRS.class,    CompoundCRS.class,     
EngineeringCRS.class,
-                                 DerivedCRS.class,     TemporalCRS.class,     
ParametricCRS.class},     // See comment below
-                new String[]   {"projected",          "geographic",          
"geocentric",
-                                "vertical",           "compound",            
"engineering",
-                                "derived",            "temporal",            
"parametric"},             // See comment below
-                "SHOW_CRS", true),
-                /*
-                 * Above declaration could omit Derived, Temporal and 
Parametric cases because they are not defined
-                 * by the EPSG repository (at least as of version 8.9). In 
particular we are not sure if EPSG would
-                 * chose to use "time" or "temporal". However, omitting those 
types slow down a lot the search for
-                 * CRS matching an existing one (even if it still work).
-                 */
-
-        new TableInfo(CoordinateSystem.class,
-                "\"Coordinate System\"",
-                "COORD_SYS_CODE",
-                "COORD_SYS_NAME",
-                "COORD_SYS_TYPE",
-                new Class<?>[] {CartesianCS.class,      EllipsoidalCS.class,   
   VerticalCS.class,      LinearCS.class,
-                                SphericalCS.class,      PolarCS.class,         
   CylindricalCS.class,
-                                TimeCS.class,           ParametricCS.class,    
   AffineCS.class},
-                new String[]   {WKTKeywords.Cartesian,  
WKTKeywords.ellipsoidal,  WKTKeywords.vertical,  WKTKeywords.linear,
-                                WKTKeywords.spherical,  WKTKeywords.polar,     
   WKTKeywords.cylindrical,
-                                WKTKeywords.temporal,   
WKTKeywords.parametric,   WKTKeywords.affine},      // Same comment as in the 
CRS case above.
-                null, false),
+    ELLIPSOID(Ellipsoid.class,
+            "\"Ellipsoid\"",
+            "ELLIPSOID_CODE",
+            "ELLIPSOID_NAME",
+            null, null, null, null, false),
 
-        new TableInfo(CoordinateSystemAxis.class,
-                "\"Coordinate Axis\" AS CA INNER JOIN \"Coordinate Axis Name\" 
AS CAN " +
-                                    "ON 
CA.COORD_AXIS_NAME_CODE=CAN.COORD_AXIS_NAME_CODE",
-                "COORD_AXIS_CODE",
-                "COORD_AXIS_NAME",
-                null, null, null, null, false),
+    /**
+     * Information about the "Prime Meridian" table.
+     */
+    PRIME_MERIDIAN(PrimeMeridian.class,
+            "\"Prime Meridian\"",
+            "PRIME_MERIDIAN_CODE",
+            "PRIME_MERIDIAN_NAME",
+            null, null, null, null, false),
 
-        DATUM = new TableInfo(Datum.class,
-                "\"Datum\"",
-                "DATUM_CODE",
-                "DATUM_NAME",
-                "DATUM_TYPE",
-                new Class<?>[] { GeodeticDatum.class,  VerticalDatum.class,   
EngineeringDatum.class,
-                                 TemporalDatum.class,  ParametricDatum.class},
-                new String[]   {"geodetic",           "vertical",            
"engineering",
-                                "temporal",           "parametric"},         
// Same comment as in the CRS case above.
-                null, true),
+    /**
+     * Information about the "Coordinate_Operation" table.
+     */
+    OPERATION(CoordinateOperation.class,
+            "\"Coordinate_Operation\"",
+            "COORD_OP_CODE",
+            "COORD_OP_NAME",
+            "COORD_OP_TYPE",
+            new Class<?>[] { Conversion.class, Transformation.class},
+            new String[]   {"conversion",     "transformation"},
+            "SHOW_OPERATION", true),
 
-        ELLIPSOID = new TableInfo(Ellipsoid.class,
-                "\"Ellipsoid\"",
-                "ELLIPSOID_CODE",
-                "ELLIPSOID_NAME",
-                null, null, null, null, false),
+    /**
+     * Information about the "Coordinate_Operation Method" table.
+     */
+    METHOD(OperationMethod.class,
+            "\"Coordinate_Operation Method\"",
+            "COORD_OP_METHOD_CODE",
+            "COORD_OP_METHOD_NAME",
+            null, null, null, null, false),
 
-        new TableInfo(PrimeMeridian.class,
-                "\"Prime Meridian\"",
-                "PRIME_MERIDIAN_CODE",
-                "PRIME_MERIDIAN_NAME",
-                null, null, null, null, false),
+    /**
+     * Information about the "Coordinate_Operation Parameter" table.
+     */
+    PARAMETER(ParameterDescriptor.class,
+            "\"Coordinate_Operation Parameter\"",
+            "PARAMETER_CODE",
+            "PARAMETER_NAME",
+            null, null, null, null, false),
 
-        new TableInfo(CoordinateOperation.class,
-                "\"Coordinate_Operation\"",
-                "COORD_OP_CODE",
-                "COORD_OP_NAME",
-                "COORD_OP_TYPE",
-                new Class<?>[] { Conversion.class, Transformation.class},
-                new String[]   {"conversion",     "transformation"},
-                "SHOW_OPERATION", true),
+    /**
+     * Information about the "Extent" table.
+     */
+    EXTENT(Extent.class,
+            "\"Extent\"",
+            "EXTENT_CODE",
+            "EXTENT_NAME",
+            null, null, null, null, false),
 
-        new TableInfo(OperationMethod.class,
-                "\"Coordinate_Operation Method\"",
-                "COORD_OP_METHOD_CODE",
-                "COORD_OP_METHOD_NAME",
-                null, null, null, null, false),
+    /**
+     * Information about the "Coordinate System" table.
+     */
+    CS(CoordinateSystem.class,
+            "\"Coordinate System\"",
+            "COORD_SYS_CODE",
+            "COORD_SYS_NAME",
+            "COORD_SYS_TYPE",
+            new Class<?>[] {CartesianCS.class,      EllipsoidalCS.class,      
VerticalCS.class,      LinearCS.class,
+                            SphericalCS.class,      PolarCS.class,            
CylindricalCS.class,
+                            TimeCS.class,           ParametricCS.class,       
AffineCS.class},
+            new String[]   {WKTKeywords.Cartesian,  WKTKeywords.ellipsoidal,  
WKTKeywords.vertical,  WKTKeywords.linear,
+                            WKTKeywords.spherical,  WKTKeywords.polar,        
WKTKeywords.cylindrical,
+                            WKTKeywords.temporal,   WKTKeywords.parametric,   
WKTKeywords.affine},      // Same comment as in the CRS case above.
+            null, false),
 
-        new TableInfo(ParameterDescriptor.class,
-                "\"Coordinate_Operation Parameter\"",
-                "PARAMETER_CODE",
-                "PARAMETER_NAME",
-                null, null, null, null, false),
+    /**
+     * Information about the "Coordinate Axis" table.
+     */
+    AXIS(CoordinateSystemAxis.class,
+            "\"Coordinate Axis\" AS CA INNER JOIN \"Coordinate Axis Name\" AS 
CAN " +
+                                "ON 
CA.COORD_AXIS_NAME_CODE=CAN.COORD_AXIS_NAME_CODE",
+            "COORD_AXIS_CODE",
+            "COORD_AXIS_NAME",
+            null, null, null, null, false),
 
-        new TableInfo(Unit.class,
-                "\"Unit of Measure\"",
-                "UOM_CODE",
-                "UNIT_OF_MEAS_NAME",
-                null, null, null, null, false),
-    };
+    /**
+     * Information about the "Unit of Measure" table.
+     */
+    UNIT(Unit.class,
+            "\"Unit of Measure\"",
+            "UOM_CODE",
+            "UNIT_OF_MEAS_NAME",
+            null, null, null, null, false);
 
     /**
      * The class of object to be created (usually a GeoAPI interface).
@@ -170,6 +199,7 @@ final class TableInfo {
 
     /**
      * The table name in mixed-case and without quotes.
+     * This name can be converted to the actual table names by {@link 
SQLTranslator#toActualTableName(String)}.
      */
     final String table;
 
@@ -186,7 +216,7 @@ final class TableInfo {
     final String codeColumn;
 
     /**
-     * Column name for the name (usually with the {@code "_NAME"} suffix), or 
{@code null}.
+     * Column name for the name (usually with the {@code "_NAME"} suffix).
      */
     final String nameColumn;
 
@@ -222,7 +252,7 @@ final class TableInfo {
      * @param type        the class of object to be created (usually a GeoAPI 
interface).
      * @param fromClause  The <abbr>SQL</abbr> fragment to use in the {@code 
FROM} clause, including quotes.
      * @param codeColumn  column name for the code (usually with the {@code 
"_CODE"} suffix).
-     * @param nameColumn  column name for the name (usually with the {@code 
"_NAME"} suffix), or {@code null}.
+     * @param nameColumn  column name for the name (usually with the {@code 
"_NAME"} suffix).
      * @param typeColumn  column type for the type (usually with the {@code 
"_TYPE"} suffix), or {@code null}.
      * @param subTypes    sub-interfaces of {@link #type} to handle, or {@code 
null} if none.
      * @param typeNames   names of {@code subTypes} in the database, or {@code 
null} if none.
@@ -243,30 +273,16 @@ final class TableInfo {
         this.typeNames  = typeNames;
         this.showColumn = showColumn;
         this.areaOfUse  = areaOfUse;
-        table = fromClause.substring(fromClause.indexOf('"') + 1, 
fromClause.lastIndexOf('"')).intern();
+        final int start = fromClause.indexOf('"') + 1;
+        table = fromClause.substring(start, fromClause.indexOf('"', 
start)).intern();
     }
 
     /**
-     * Returns the class of objects created from the given table. The given 
table name should be one
-     * of the values enumerated in the {@code "Table Name"} types of the 
{@code Prepare.sql} file.
-     * The name may be prefixed by {@code "epsg_"} and may contain 
abbreviations of the full name.
-     * For example, {@code "epsg_coordoperation"} is considered as a match for 
{@code "Coordinate_Operation"}.
-     *
-     * @param  table  mixed-case name of an <abbr>EPSG</abbr> table.
-     * @return name of the class of objects created from the given table.
+     * Returns whether this enumeration value can be used when looking a table 
by an object type.
+     * This method returns {@code false} for types that are too generic.
      */
-    static Optional<String> getObjectClassName(String table) {
-        if (table != null) {
-            if (table.startsWith(SQLTranslator.TABLE_PREFIX)) {
-                table = table.substring(SQLTranslator.TABLE_PREFIX.length());
-            }
-            for (final TableInfo info : EPSG) {
-                if (CharSequences.isAcronymForWords(table, info.table)) {
-                    return Optional.of(info.type.getSimpleName());
-                }
-            }
-        }
-        return Optional.empty();
+    final boolean isCandidate() {
+        return type != IdentifiedObject.class;
     }
 
     /**
@@ -326,4 +342,23 @@ final class TableInfo {
         }
         return type;
     }
+
+    /**
+     * Verifies that the given <abbr>SQL</abbr> statement contains the 
expected table and columns.
+     * This method is for assertions only. It may either returns {@code false} 
if the query is not
+     * valid, or throws an {@link AssertionError} itself for providing more 
details.
+     *
+     * @param  sql  the <abbr>SQL</abbr> statement to validate.
+     * @return whether the statement is valid.
+     */
+    final boolean validate(final String sql) {
+        if (sql.contains(table)) {
+            if (type.isAssignableFrom(IdentifiedObject.class)) {
+                if (!sql.contains(codeColumn)) throw new 
AssertionError(codeColumn);
+                if (!sql.contains(nameColumn)) throw new 
AssertionError(nameColumn);
+            }
+            return true;
+        }
+        return false;
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/TableInfoTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/TableInfoTest.java
index e3b30f74d9..943641f75d 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/TableInfoTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/TableInfoTest.java
@@ -35,12 +35,16 @@ public final class TableInfoTest extends TestCase {
     }
 
     /**
-     * Tests {@link TableInfo#getObjectClassName(String)}.
+     * Validates the enumeration values.
      */
     @Test
-    public void testgGetObjectClassName() {
-        assertEquals("Datum",                     
TableInfo.getObjectClassName("epsg_datum").orElseThrow());
-        assertEquals("Ellipsoid",                 
TableInfo.getObjectClassName("epsg_ellipsoid").orElseThrow());
-        assertEquals("CoordinateReferenceSystem", 
TableInfo.getObjectClassName("epsg_coordinatereferencesystem").orElseThrow());
+    public void validate() {
+        for (TableInfo info : TableInfo.values()) {
+            assertNotNull(info.type);
+            assertNotNull(info.table);
+            assertNotNull(info.codeColumn);
+            assertNotNull(info.nameColumn);
+            assertTrue(info.fromClause.contains(info.table));
+        }
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/AxisType.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/AxisType.java
index a66b1c9e5c..fca5b97b5c 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/AxisType.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/AxisType.java
@@ -26,7 +26,7 @@ import org.apache.sis.measure.Units;
 
 
 /**
- * Type of coordinate system axis, in the order they should appears for a 
"normalized" coordinate reference system.
+ * Type of coordinate system axis, in the order they should appear for a 
"normalized" coordinate reference system.
  * The enumeration name matches the name of the {@code "axis"} attribute in 
CF-convention.
  * Enumeration order is the desired order of coordinate values.
  *

Reply via email to