You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2025/08/14 11:59:59 UTC

(sis) branch geoapi-4.0 updated: Update the EPSG database installer: Contains also the following changes:

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 967360a5f7 Update the EPSG database installer: Contains also the following changes:
967360a5f7 is described below

commit 967360a5f7c37127c9e1cec5a00605c5246168f0
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Aug 10 13:43:00 2025 +0200

    Update the EPSG database installer:
    Contains also the following changes:
    
    * Replax `Datum` / `DatumEnsemble` comparison in `ALLOW_VARIANT` mode.
    * Rename `epsg_` table names to the camel case convention used by Apache SIS.
    * Use a consistent naming convention for the tables that are new in EPSG 10+.
    * Rename the enumerations to the final names that they have in the database.
    * Finer conversion to `VARCHAR` for databases that do not support enumerations.
    * Replace most of `ScriptRunner.isFooSupported` flags by `Dialect` enumeration.
    * Add `StringBuilders.trimWhitespaces` utility method.
    * Exclude deprecated or unused columns in EPSG scripts.
---
 .../org/apache/sis/metadata/sql/Installer.java     |   2 +-
 .../org/apache/sis/metadata/sql/privy/Dialect.java |  78 +++-
 .../sis/metadata/sql/privy/ScriptRunner.java       | 274 ++++--------
 .../apache/sis/metadata/sql/privy/Supports.java    |  41 +-
 .../main/org/apache/sis/temporal/TemporalDate.java |   2 +-
 .../org/apache/sis/metadata/sql/TestDatabase.java  |   2 +-
 .../sis/metadata/sql/privy/ScriptRunnerTest.java   |   9 +-
 .../sis/metadata/sql/privy/SupportsTest.java}      |  34 +-
 .../sis/referencing/crs/AbstractSingleCRS.java     |  32 +-
 .../org/apache/sis/referencing/cs/AbstractCS.java  |   2 +-
 .../sis/referencing/datum/AbstractDatum.java       |   2 +-
 .../referencing/factory/sql/AuthorityCodes.java    |   2 +-
 .../referencing/factory/sql/EPSGCodeFinder.java    |   2 +-
 .../referencing/factory/sql/EPSGDataAccess.java    |  24 +-
 .../sis/referencing/factory/sql/EPSGFactory.java   |  54 +--
 .../sis/referencing/factory/sql/EPSGInstaller.java | 190 ++------
 .../sis/referencing/factory/sql/EPSG_Finish.sql    | 117 ++---
 .../sis/referencing/factory/sql/EPSG_Prepare.sql   |  32 +-
 .../factory/sql/InstallationScriptProvider.java    |  31 +-
 .../sis/referencing/factory/sql/SQLTranslator.java | 137 +++---
 .../sis/referencing/factory/sql/TableInfo.java     |  36 +-
 .../operation/AbstractCoordinateOperation.java     |   2 +-
 .../referencing/factory/sql/EPSGInstallerTest.java |  40 +-
 .../main/org/apache/sis/pending/jdk/JDK22.java     |  11 +
 .../main/org/apache/sis/util/ComparisonMode.java   |  62 ++-
 .../main/org/apache/sis/util/StringBuilders.java   |  23 +-
 .../org/apache/sis/util/StringBuildersTest.java    |  27 +-
 .../apache/sis/storage/geopackage/GpkgStore.java   |   2 +-
 .../sis/referencing/factory/sql/epsg/Clear.sql     |  43 ++
 .../factory/sql/epsg/DataScriptFormatter.java      | 495 +++++++--------------
 .../referencing/factory/sql/epsg/DebugTools.sql    |  17 +-
 .../sis/referencing/factory/sql/epsg/README.md     |  23 +-
 .../referencing/factory/sql/epsg/TableValues.java  | 364 +++++++++++++++
 .../sis/referencing/factory/sql/epsg/Updater.java  |  73 +++
 34 files changed, 1296 insertions(+), 989 deletions(-)

diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Installer.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Installer.java
index 853d1a5889..82d78f034c 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Installer.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Installer.java
@@ -51,7 +51,7 @@ final class Installer extends ScriptRunner {
      * @throws SQLException if an error occurred while executing a SQL statement.
      */
     Installer(final Connection connection) throws SQLException {
-        super(connection, 100);
+        super(connection, null, 100);
         if (isEnumTypeSupported) {
             enumTypes = null;
         } else {
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java
index 816741e419..f4f6e0e7e6 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java
@@ -36,9 +36,9 @@ public enum Dialect {
      * @see DatabaseMetaData#supportsANSI92EntryLevelSQL()
      */
     ANSI(null, Supports.ALTER_TABLE_WITH_ADD_CONSTRAINT
-             | Supports.JAVA_TIME
              | Supports.READ_ONLY_UPDATE
              | Supports.CONCURRENCY
+             | Supports.JAVA_TIME
              | Supports.SRID),
 
     /**
@@ -57,9 +57,9 @@ public enum Dialect {
      * It also have a {@code SHUTDOWN} command which is specific to HSQLDB.
      */
     HSQL("hsqldb", Supports.ALTER_TABLE_WITH_ADD_CONSTRAINT
-                 | Supports.JAVA_TIME
                  | Supports.READ_ONLY_UPDATE
-                 | Supports.CONCURRENCY),
+                 | Supports.CONCURRENCY
+                 | Supports.JAVA_TIME),
 
     /**
      * The database uses PostgreSQL syntax. This is ANSI, but provided an a separated
@@ -67,18 +67,21 @@ public enum Dialect {
      */
     POSTGRESQL("postgresql", Supports.TABLE_INHERITANCE
                            | Supports.ALTER_TABLE_WITH_ADD_CONSTRAINT
-                           | Supports.JAVA_TIME
+                           | Supports.GRANT_USAGE_ON_SCHEMA
+                           | Supports.GRANT_SELECT_ON_TABLE
+                           | Supports.COMMENT
                            | Supports.READ_ONLY_UPDATE
                            | Supports.CONCURRENCY
+                           | Supports.JAVA_TIME
                            | Supports.SRID),
 
     /**
      * The database uses Oracle syntax. This is ANSI, but without {@code "AS"} keyword.
      */
     ORACLE("oracle", Supports.ALTER_TABLE_WITH_ADD_CONSTRAINT
-                   | Supports.JAVA_TIME
                    | Supports.READ_ONLY_UPDATE
                    | Supports.CONCURRENCY
+                   | Supports.JAVA_TIME
                    | Supports.SRID),
 
     /**
@@ -133,7 +136,7 @@ public enum Dialect {
     }
 
     /**
-     * Whether this dialect support table inheritance.
+     * Whether this dialect supports table inheritance.
      */
     public final boolean supportsTableInheritance() {
         return (flags & Supports.TABLE_INHERITANCE) != 0;
@@ -150,7 +153,7 @@ public enum Dialect {
     }
 
     /**
-     * Whether this dialect support adding table constraints after creation.
+     * Whether this dialect supports adding table constraints after creation.
      * This feature is not yet supported in SQLite.
      *
      * @see DatabaseMetaData#supportsAlterTableWithAddColumn()
@@ -160,15 +163,50 @@ public enum Dialect {
     }
 
     /**
-     * Whether the JDBC driver supports conversions from objects to {@code java.time} API.
-     * The JDBC 4.2 specification provides a mapping from {@link java.sql.Types} to temporal objects.
-     * The specification suggests that {@link java.sql.ResultSet#getObject(int, Class)} should accept
-     * those temporal types in the {@link Class} argument, but not all drivers support that.
+     * {@code true} if the database supports {@code "GRANT USAGE ON SCHEMA"} statements.
+     * Read-only permissions are typically granted to {@code PUBLIC}.
+     * Example:
      *
-     * @see <a href="https://jcp.org/aboutJava/communityprocess/maintenance/jsr221/JDBC4.2MR-January2014.pdf">JDBC Maintenance Release 4.2</a>
+     * {@snippet lang="sql" :
+     *     GRANT USAGE ON SCHEMA metadata TO PUBLIC;
+     *     }
      */
-    public final boolean supportsJavaTime() {
-        return (flags & Supports.JAVA_TIME) != 0;
+    public final boolean supportsGrantUsageOnSchema() {
+        return (flags & Supports.GRANT_USAGE_ON_SCHEMA) != 0;
+    }
+
+    /**
+     * {@code true} if the database supports {@code "GRANT SELECT ON TABLE"} statements.
+     * Read-only permissions are typically granted to {@code PUBLIC}.
+     * Example:
+     *
+     * {@snippet lang="sql" :
+     *     GRANT SELECT ON TABLE "Coordinate Reference System" TO PUBLIC;
+     *     }
+     *
+     */
+    public final boolean supportsGrantSelectOnTable() {
+        return (flags & Supports.GRANT_SELECT_ON_TABLE) != 0;
+    }
+
+    /**
+     * Whether this dialect supports all the {@code "GRANT … ON …"} features documented in this class.
+     */
+    public final boolean supportsAllGrants() {
+        return (flags & (Supports.GRANT_USAGE_ON_SCHEMA | Supports.GRANT_SELECT_ON_TABLE))
+                     == (Supports.GRANT_USAGE_ON_SCHEMA | Supports.GRANT_SELECT_ON_TABLE);
+    }
+
+    /**
+     * {@code true} if the database supports the {@code COMMENT} statement.
+     * Example:
+     *
+     * {@snippet lang="sql" :
+     *     COMMENT ON SCHEMA metadata IS 'ISO 19115 metadata';
+     *     }
+     */
+    public final boolean supportsComment() {
+        return (flags & Supports.COMMENT) != 0;
     }
 
     /**
@@ -187,6 +225,18 @@ public enum Dialect {
         return (flags & Supports.CONCURRENCY) != 0;
     }
 
+    /**
+     * Whether the JDBC driver supports conversions from objects to {@code java.time} API.
+     * The JDBC 4.2 specification provides a mapping from {@link java.sql.Types} to temporal objects.
+     * The specification suggests that {@link java.sql.ResultSet#getObject(int, Class)} should accept
+     * those temporal types in the {@link Class} argument, but not all drivers support that.
+     *
+     * @see <a href="https://jcp.org/aboutJava/communityprocess/maintenance/jsr221/JDBC4.2MR-January2014.pdf">JDBC Maintenance Release 4.2</a>
+     */
+    public final boolean supportsJavaTime() {
+        return (flags & Supports.JAVA_TIME) != 0;
+    }
+
     /**
      * Whether the spatial extension supports <abbr>SRID</abbr> in {@code ST_*} functions.
      */
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/ScriptRunner.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/ScriptRunner.java
index 4883581f48..788d1836b3 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/ScriptRunner.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/ScriptRunner.java
@@ -21,7 +21,6 @@ import java.util.HashMap;
 import java.util.Locale;
 import java.util.regex.Pattern;
 import java.util.regex.Matcher;
-import java.util.function.BiFunction;
 import java.io.FileNotFoundException;
 import java.io.EOFException;
 import java.io.IOException;
@@ -56,14 +55,6 @@ import org.apache.sis.util.resources.Errors;
  * @author  Johann Sorel (Geomatys)
  */
 public class ScriptRunner implements AutoCloseable {
-    /**
-     * The database user having read (not write) permissions.
-     *
-     * @see #isGrantOnSchemaSupported
-     * @see #isGrantOnTableSupported
-     */
-    protected static final String PUBLIC = "PUBLIC";
-
     /**
      * The sequence for SQL comments. Leading lines starting by those characters will be ignored.
      */
@@ -87,16 +78,6 @@ public class ScriptRunner implements AutoCloseable {
      */
     private static final char END_OF_STATEMENT = ';';
 
-    /**
-     * The characters for escaping a portion of the SQL script. This is used by PostgreSQL
-     * for the definition of triggers. Those characters should appear at the beginning of
-     * a line (ignoring whitespaces), because the text before it will not be parsed.
-     *
-     * <p>This string shall not begin with a whitespace or
-     * {@linkplain Character#isUnicodeIdentifierPart(int) Unicode identifier part}.</p>
-     */
-    private static final String ESCAPE = "$BODY$";
-
     /**
      * The presumed dialect spoken by the database.
      */
@@ -108,7 +89,7 @@ public class ScriptRunner implements AutoCloseable {
      *
      * @see #addReplacement(String, String)
      */
-    private final Map<String,String> replacements = new HashMap<>();
+    private final Map<String,String> replacements;
 
     /**
      * The quote character for identifiers actually used in the database,
@@ -136,73 +117,6 @@ public class ScriptRunner implements AutoCloseable {
      */
     protected final boolean isEnumTypeSupported;
 
-    /**
-     * {@code true} if the database supports catalogs.
-     */
-    protected final boolean isCatalogSupported;
-
-    /**
-     * {@code true} if the database supports schemas.
-     */
-    protected final boolean isSchemaSupported;
-
-    /**
-     * {@code true} if the database supports {@code "GRANT USAGE ON SCHEMA"} statements.
-     * Read-only permissions are typically granted to {@link #PUBLIC}.
-     * Example:
-     *
-     * {@snippet lang="sql" :
-     *     GRANT USAGE ON SCHEMA metadata TO PUBLIC;
-     *     }
-     *
-     * @see #statementsToSkip
-     */
-    protected final boolean isGrantOnSchemaSupported;
-
-    /**
-     * {@code true} if the database supports {@code "GRANT SELECT ON TABLE"} statements.
-     * Read-only permissions are typically granted to {@link #PUBLIC}.
-     * Example:
-     *
-     * {@snippet lang="sql" :
-     *     GRANT SELECT ON TABLE epsg_coordinatereferencesystem TO PUBLIC;
-     *     }
-     *
-     * @see #statementsToSkip
-     */
-    protected final boolean isGrantOnTableSupported;
-
-    /**
-     * {@code true} if the database supports the {@code COMMENT} statement.
-     * Example:
-     *
-     * {@snippet lang="sql" :
-     *     COMMENT ON SCHEMA metadata IS 'ISO 19115 metadata';
-     *     }
-     *
-     * @see #statementsToSkip
-     */
-    protected final boolean isCommentSupported;
-
-    /**
-     * {@code true} if the following instruction shall be executed
-     * (assuming that the PostgreSQL {@code "plpgsql"} language is desired):
-     *
-     * {@code sql
-     *   CREATE TRUSTED PROCEDURAL LANGUAGE 'plpgsql'
-     *     HANDLER plpgsql_call_handler
-     *     VALIDATOR plpgsql_validator;
-     * }
-     *
-     * <p>Notes per database product:</p>
-     * <ul>
-     *   <li><b>PostgreSQL:</b> {@code true} only for database prior to version 9.
-     *       Starting at version 9, the language is installed by default.</li>
-     *   <li><b>Other databases:</b> {@code false} because not supported.</li>
-     * </ul>
-     */
-    protected final boolean isCreateLanguageRequired;
-
     /**
      * The maximum number of rows allowed per {@code "INSERT"} statement.
      * This is 1 if the database does not support multi-rows insertion.
@@ -223,9 +137,9 @@ public class ScriptRunner implements AutoCloseable {
      *
      * <ul>
      *   <li>{@link #isEnumTypeSupported} for {@code "CREATE TYPE …"} or {@code "CREATE CAST …"} statements.</li>
-     *   <li>{@link #isGrantOnSchemaSupported} for {@code "GRANT USAGE ON SCHEMA …"} statements.</li>
-     *   <li>{@link #isGrantOnTableSupported} for {@code "GRANT SELECT ON TABLE …"} statements.</li>
-     *   <li>{@link #isCommentSupported} for {@code "COMMENT ON …"} statements.</li>
+     *   <li>{@link Dialect#supportsGrantUsageOnSchema} for {@code "GRANT USAGE ON SCHEMA …"} statements.</li>
+     *   <li>{@link Dialect#supportsGrantSelectOnTable} for {@code "GRANT SELECT ON TABLE …"} statements.</li>
+     *   <li>{@link Dialect#supportsComment} for {@code "COMMENT ON …"} statements.</li>
      * </ul>
      */
     private Matcher statementsToSkip;
@@ -260,52 +174,50 @@ public class ScriptRunner implements AutoCloseable {
      *
      * <p>Some {@code maxRowsPerInsert} parameter values of interest:</p>
      * <ul>
-     *   <li>A value of 0 means to create only the schemas without inserting any data in them.</li>
+     *   <li>A value of 0 means to create only the schema without inserting any data in them.</li>
      *   <li>A value of 1 means to use one separated {@code INSERT INTO} statement for each row, which may be slow.</li>
      *   <li>A value of 100 is a value which have been found empirically as giving good results.</li>
      *   <li>A value of {@link Integer#MAX_VALUE} means to not perform any attempt to limit the number of rows in an
      *       {@code INSERT INTO} statement. Note that this causes {@link StackOverflowError} in some JDBC driver.</li>
      * </ul>
      *
+     * The {@code schemaToCreate} argument is ignored if not supported by the database.
+     *
      * @param  connection        the connection to the database.
+     * @param  schemaToCreate    schema to create and set as the default schema, or {@code null} if none.
      * @param  maxRowsPerInsert  maximum number of rows per {@code "INSERT INTO"} statement.
      * @throws SQLException if an error occurred while creating a SQL statement.
      */
-    public ScriptRunner(final Connection connection, final int maxRowsPerInsert) throws SQLException {
+    public ScriptRunner(final Connection connection, final String schemaToCreate, final int maxRowsPerInsert) throws SQLException {
         ArgumentChecks.ensurePositive("maxRowsPerInsert", maxRowsPerInsert);
-        final DatabaseMetaData metadata = connection.getMetaData();
-        this.maxRowsPerInsert   = maxRowsPerInsert;
-        this.dialect            = Dialect.guess(metadata);
-        this.identifierQuote    = metadata.getIdentifierQuoteString();
-        this.isSchemaSupported  = metadata.supportsSchemasInTableDefinitions() &&
-                                  metadata.supportsSchemasInDataManipulation();
-        this.isCatalogSupported = metadata.supportsCatalogsInTableDefinitions() &&
-                                  metadata.supportsCatalogsInDataManipulation();
-        this.statement          = connection.createStatement();
+        final DatabaseMetaData metadata;
+        this.maxRowsPerInsert = maxRowsPerInsert;
+        replacements     = new HashMap<>();
+        metadata         = connection.getMetaData();
+        dialect          = Dialect.guess(metadata);
+        identifierQuote  = metadata.getIdentifierQuoteString();
+        if (schemaToCreate != null && metadata.supportsSchemasInTableDefinitions()) {
+            try (Statement stmt = connection.createStatement()) {
+                stmt.executeUpdate("CREATE SCHEMA " + identifierQuote + schemaToCreate + identifierQuote);
+                if (dialect.supportsGrantUsageOnSchema()) {
+                    stmt.executeUpdate("GRANT USAGE ON SCHEMA " + identifierQuote + schemaToCreate + identifierQuote + " TO PUBLIC");
+                }
+            }
+            connection.setSchema(schemaToCreate);   // Must be set before the next call to `createStatement()` below.
+        }
+        statement = connection.createStatement();
         switch (dialect) {
             default: {
-                isEnumTypeSupported      = false;
-                isGrantOnSchemaSupported = false;
-                isGrantOnTableSupported  = false;
-                isCreateLanguageRequired = false;
-                isCommentSupported       = false;
+                isEnumTypeSupported = false;
                 break;
             }
             case POSTGRESQL: {
                 final int version = metadata.getDatabaseMajorVersion();
-                isEnumTypeSupported      = (version == 8) ? metadata.getDatabaseMinorVersion() >= 4 : version >= 8;
-                isGrantOnSchemaSupported = true;
-                isGrantOnTableSupported  = true;
-                isCreateLanguageRequired = (version < 9);
-                isCommentSupported       = true;
+                isEnumTypeSupported = (version == 8) ? metadata.getDatabaseMinorVersion() >= 4 : version >= 8;
                 break;
             }
             case HSQL: {
-                isEnumTypeSupported      = false;
-                isGrantOnSchemaSupported = false;
-                isGrantOnTableSupported  = false;
-                isCreateLanguageRequired = false;
-                isCommentSupported       = false;
+                isEnumTypeSupported = false;
                 /*
                  * HSQLDB stores tables in memory by default. For storing the tables on files, we have to
                  * use "CREATE CACHED TABLE" statement, which is HSQL-specific. For avoiding SQL dialect,
@@ -325,18 +237,18 @@ public class ScriptRunner implements AutoCloseable {
         if (!isEnumTypeSupported) {
             addStatementToSkip("CREATE\\s+(?:TYPE|CAST)\\s+.*");
         }
-        if (!isGrantOnSchemaSupported || !isGrantOnTableSupported) {
+        if (!dialect.supportsAllGrants()) {
             addStatementToSkip("GRANT\\s+\\w+\\s+ON\\s+");
-            if (isGrantOnSchemaSupported) {
+            if (dialect.supportsGrantUsageOnSchema()) {
                 regexOfStmtToSkip.append("TABLE");
-            } else if (isGrantOnTableSupported) {
+            } else if (dialect.supportsGrantSelectOnTable()) {
                 regexOfStmtToSkip.append("SCHEMA");
             } else {
                 regexOfStmtToSkip.append("(?:TABLE|SCHEMA)");
             }
             regexOfStmtToSkip.append("\\s+.*");
         }
-        if (!isCommentSupported) {
+        if (!dialect.supportsComment()) {
             addStatementToSkip("COMMENT\\s+ON\\s+.*");
         }
         if (!dialect.supportsAlterTableWithAddConstraint()) {
@@ -359,9 +271,9 @@ public class ScriptRunner implements AutoCloseable {
      *
      * <ul>
      *   <li>{@code "CREATE TYPE …"} or {@code "CREATE CAST …"} if {@link #isEnumTypeSupported} is {@code false}.</li>
-     *   <li>{@code "GRANT USAGE ON SCHEMA …"} if {@link #isGrantOnSchemaSupported} is {@code false}.</li>
-     *   <li>{@code "GRANT SELECT ON TABLE …"} if {@link #isGrantOnTableSupported} is {@code false}.</li>
-     *   <li>{@code "COMMENT ON …"} if {@link #isCommentSupported} is {@code false}.</li>
+     *   <li>{@code "GRANT USAGE ON SCHEMA …"} if {@link Dialect#supportsGrantUsageOnSchema} is {@code false}.</li>
+     *   <li>{@code "GRANT SELECT ON TABLE …"} if {@link Dialect#supportsGrantSelectOnTable} is {@code false}.</li>
+     *   <li>{@code "COMMENT ON …"} if {@link Dialect#supportsComment} is {@code false}.</li>
      * </ul>
      *
      * This method can be invoked for ignoring some additional statements.
@@ -380,8 +292,10 @@ public class ScriptRunner implements AutoCloseable {
     }
 
     /**
-     * Declares that a word in the SQL script needs to be replaced by the given word.
-     * The replacement is performed only for occurrences outside identifiers or texts.
+     * Declares that a word in the <abbr>SQL</abbr> script needs to be replaced by the given word.
+     * The replacement is performed only for occurrences outside quoted identifiers or texts.
+     * For replacement of texts or identifiers, see {@link #editText(StringBuilder, int, int)}
+     * and {@link #editQuotedIdentifier(StringBuilder, int, int)} instead.
      *
      * <h4>Example</h4>
      * This is used for mapping the <abbr>EPSG</abbr> table names from the mixed-cases convention used in
@@ -393,7 +307,7 @@ public class ScriptRunner implements AutoCloseable {
      * understood by the database. For example, if a database does not support the {@code "TEXT"} data type,
      * this method can be used for replacing {@code "TEXT"} by {@code "LONG VARCHAR"}.</p>
      *
-     * <h4>Limitation</h3>
+     * <h4>Limitation</h4>
      * The {@code inScript} word to replace must be a single word with no space.
      * If the text to replace contains two words (for example {@code "CREATE TABLE"}), then revert
      * commit {@code bceb569558bfb7e3cf1a14aaf9261e786db06856} for bringing back this functionality.
@@ -418,16 +332,6 @@ public class ScriptRunner implements AutoCloseable {
         return replacements.getOrDefault(inScript, inScript);
     }
 
-    /**
-     * For every entries in the replacements map, replaces the entry value by the value returned by
-     * {@code function(key, value)}.
-     *
-     * @param function  the function that modify the replacement mapping.
-     */
-    protected final void modifyReplacements(final BiFunction<String,String,String> function) {
-        replacements.replaceAll(function);
-    }
-
     /**
      * Runs the given SQL script.
      * Lines are read and grouped up to the terminal {@value #END_OF_STATEMENT} character, then sent to the database.
@@ -486,12 +390,14 @@ public class ScriptRunner implements AutoCloseable {
     public final int run(final String filename, final BufferedReader in) throws IOException, SQLException {
         currentFile = filename;
         currentLine = 0;
-        int     statementCount     = 0;         // For informative purpose only.
-        int     posOpeningQuote    = -1;        // -1 if we are not inside a text.
-        boolean isInsideIdentifier = false;
+        int statementCount            =  0;     // For informative purpose only.
+        int posOpeningTextQuote       = -1;     // -1 if we are not inside a text.
+        int posOpeningIdentifierQuote = -1;
         final var buffer = new StringBuilder();
+        final boolean hasReplacements = !replacements.isEmpty();
         String line;
         while ((line = in.readLine()) != null) {
+            currentLine++;
             /*
              * Ignore empty lines and comment lines, but only if they appear at the begining of the SQL statement.
              */
@@ -506,44 +412,23 @@ public class ScriptRunner implements AutoCloseable {
             } else {
                 buffer.append('\n');
             }
-            /*
-             * If we find the "$BODY$" string, copy verbatism (without any attempt to parse the lines) until
-             * the next occurrence of "$BODY$".  This simple algorithm does not allow more than one block of
-             * "$BODY$ ... $BODY$" on the same statement and presumes that the text before "$BODY$" contains
-             * nothing that need to be parsed.
-             */
-            int pos = line.indexOf(ESCAPE);
-            if (pos >= 0) {
-                pos += ESCAPE.length();
-                while ((pos = line.indexOf(ESCAPE, pos)) < 0) {
-                    buffer.append(line).append('\n');
-                    line = in.readLine();
-                    if (line == null) {
-                        throw new EOFException();
-                    }
-                    pos = 0;
-                }
-                pos += ESCAPE.length();
-                buffer.append(line, 0, pos);
-                line = line.substring(pos);
-            }
             /*
              * Copy the current line in the buffer. Then, the loop will search for words or characters to replace
              * (for example replacements of IDENTIFIER_QUOTE character by the database-specific quote character).
              * Replacements (if any) will be performed in-place in the buffer. Concequently the buffer length may
              * vary during the loop execution.
              */
-            pos = buffer.length();
+            int pos = buffer.length();
             int length = buffer.append(line).length();
 parseLine:  while (pos < length) {
                 int c = buffer.codePointAt(pos);
                 int n = Character.charCount(c);
-                if (posOpeningQuote < 0 && !isInsideIdentifier) {
+                if ((posOpeningTextQuote & posOpeningIdentifierQuote) < 0) {    // True if both positions are -1.
                     int start = pos;
                     while (Character.isUnicodeIdentifierStart(c)) {
                         /*
-                         * 'start' is the position of the first character of a Unicode identifier. Following loop
-                         * sets 'pos' to the end (exclusive) of that Unicode identifier. Variable 'c' will be set
+                         * `start` is the position of the first character of a Unicode identifier. Following loop
+                         * sets `pos` to the end (exclusive) of that Unicode identifier. Variable `c` will be set
                          * to the character after the Unicode identifier, provided that we have not reached EOL.
                          */
                         while ((pos += n) < length) {
@@ -553,17 +438,19 @@ parseLine:  while (pos < length) {
                         }
                         /*
                          * Perform in-place replacement if the Unicode identifier is one of the keys listed
-                         * in the 'replacements' map. This operation may change the buffer length. The 'pos'
+                         * in the `replacements` map. This operation may change the buffer length. The `pos`
                          * must be updated if needed for staying at position after the Unicode identifier.
                          */
-                        final String word = buffer.substring(start, pos);
-                        final String replace = replacements.get(word);
-                        if (replace != null) {
-                            length = buffer.replace(start, pos, replace).length();
-                            pos = start + replace.length();
+                        if (hasReplacements) {
+                            final String word = buffer.substring(start, pos);
+                            final String replace = replacements.get(word);
+                            if (replace != null) {
+                                length = buffer.replace(start, pos, replace).length();
+                                pos = start + replace.length();
+                            }
                         }
                         /*
-                         * Skip whitespaces and set the 'c' variable to the next character, which may be either
+                         * Skip whitespaces and set the `c` variable to the next character, which may be either
                          * another Unicode start (to be processed by the enclosing loop) or another character
                          * (to be processed by the switch statement after the enclosing loop).
                          */
@@ -583,10 +470,17 @@ parseLine:  while (pos < length) {
                      * replace the standard quote character by the database-specific one.
                      */
                     case IDENTIFIER_QUOTE: {
-                        if (posOpeningQuote < 0) {
-                            isInsideIdentifier = !isInsideIdentifier;
+                        if (posOpeningTextQuote < 0) {
                             length = buffer.replace(pos, pos + n, identifierQuote).length();
                             n = identifierQuote.length();
+                            if (posOpeningIdentifierQuote < 0) {
+                                posOpeningIdentifierQuote = pos;
+                            } else {
+                                editQuotedIdentifier(buffer, posOpeningIdentifierQuote, pos += n);
+                                pos -= length - (length = buffer.length());
+                                posOpeningIdentifierQuote = -1;
+                                continue;   // Because we already skipped the " character.
+                            }
                         }
                         break;
                     }
@@ -595,13 +489,13 @@ parseLine:  while (pos < length) {
                      * found the opening or closing character, ignoring the '' escape sequence.
                      */
                     case QUOTE: {
-                        if (!isInsideIdentifier) {
-                            if (posOpeningQuote < 0) {
-                                posOpeningQuote = pos;
+                        if (posOpeningIdentifierQuote < 0) {
+                            if (posOpeningTextQuote < 0) {
+                                posOpeningTextQuote = pos;
                             } else if ((pos += n) >= length || buffer.codePointAt(pos) != QUOTE) {
-                                editText(buffer, posOpeningQuote, pos);
+                                editText(buffer, posOpeningTextQuote, pos);
                                 pos -= length - (length = buffer.length());
-                                posOpeningQuote = -1;
+                                posOpeningTextQuote = -1;
                                 continue;   // Because we already skipped the ' character.
                             } // else found a double ' character, which means to escape it.
                         }
@@ -612,7 +506,7 @@ parseLine:  while (pos < length) {
                      * since SQL statement in JDBC are not expected to contain it.
                      */
                     case END_OF_STATEMENT: {
-                        if (posOpeningQuote < 0 && !isInsideIdentifier) {
+                        if ((posOpeningTextQuote & posOpeningIdentifierQuote) < 0) {    // True if both are -1.
                             if (CharSequences.skipLeadingWhitespaces(buffer, pos + n, length) >= length) {
                                 buffer.setLength(pos);
                             }
@@ -635,7 +529,8 @@ parseLine:  while (pos < length) {
     }
 
     /**
-     * Invoked for each text found in a SQL statement. The text, <em>including its quote characters</em>,
+     * Invoked for each single-quoted text found in a <abbr>SQL</abbr> statement.
+     * The text, <em>including its single quote characters</em>,
      * is the {@code sql} substring from index {@code lower} inclusive to {@code upper} exclusive.
      * Subclasses can override this method if they wish to modify the text content.
      * Modifications are applied directly in the given {@code sql} buffer.
@@ -647,6 +542,20 @@ parseLine:  while (pos < length) {
     protected void editText(final StringBuilder sql, final int lower, final int upper) {
     }
 
+    /**
+     * Invoked for each double-quoted identifier found in a <abbr>SQL</abbr> statement.
+     * The identifier, <em>including its double quote characters</em>,
+     * is the {@code sql} substring from index {@code lower} inclusive to {@code upper} exclusive.
+     * Subclasses can override this method if they wish to modify the identifier.
+     * Modifications are applied directly in the given {@code sql} buffer.
+     *
+     * @param  sql    the whole SQL statement.
+     * @param  lower  index of the opening quote character (usually {@code "}) of the identifier in {@code sql}.
+     * @param  upper  index after the closing quote character (usually {@code "}) of the identifier in {@code sql}.
+     */
+    protected void editQuotedIdentifier(final StringBuilder sql, final int lower, final int upper) {
+    }
+
     /**
      * Returns {@code true} if the given fragment seems outside identifier quotes or text quotes.
      * The given fragment must be the beginning or the end of an SQL statement, or be bounded by
@@ -763,7 +672,7 @@ parseLine:  while (pos < length) {
                         int nrows = maxRowsPerInsert;
                         int begin = endOfLine + 1;
                         while ((endOfLine = subSQL.indexOf('\n', ++endOfLine)) >= 0) {
-                            if (--nrows == 0) {    // Extract lines until we have reached the 'maxRowsPerInsert' amount.
+                            if (--nrows == 0) {    // Extract lines until we have reached the `maxRowsPerInsert` amount.
                                 int end = endOfLine;
                                 if (subSQL.charAt(end - 1) == ',') {
                                     end--;
@@ -810,8 +719,7 @@ parseLine:  while (pos < length) {
     public String status(final Locale locale) {
         String position = null;
         if (currentFile != null) {
-            position = Errors.forLocale(locale).getString(Errors.Keys.ErrorInFileAtLine_2, currentFile,
-                    (currentLine != 0) ? currentLine : '?');
+            position = Errors.forLocale(locale).getString(Errors.Keys.ErrorInFileAtLine_2, currentFile, currentLine);
         }
         if (currentSQL != null) {
             final var buffer = new StringBuilder();
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
index d03cbe4f97..4be4e20293 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Supports.java
@@ -27,7 +27,7 @@ final class Supports {
     /**
      * Whether this dialect support table inheritance.
      */
-    static final int TABLE_INHERITANCE = 1;
+    public static final int TABLE_INHERITANCE = 0x001;
 
     /**
      * Whether child tables inherit the index of their parent tables.
@@ -35,7 +35,7 @@ final class Supports {
      *
      * @see <a href="https://issues.apache.org/jira/browse/SIS-358">SIS-358</a>
      */
-    static final int INDEX_INHERITANCE = 2;
+    public static final int INDEX_INHERITANCE = 0x002;
 
     /**
      * Whether this dialect support adding table constraints after creation.
@@ -43,33 +43,48 @@ final class Supports {
      *
      * @see DatabaseMetaData#supportsAlterTableWithAddColumn()
      */
-    static final int ALTER_TABLE_WITH_ADD_CONSTRAINT = 4;
+    public static final int ALTER_TABLE_WITH_ADD_CONSTRAINT = 0x004;
 
     /**
-     * Whether the JDBC driver supports conversions from objects to {@code java.time} API.
-     * The JDBC 4.2 specification provides a mapping from {@link java.sql.Types} to temporal objects.
-     * The specification suggests that {@link java.sql.ResultSet#getObject(int, Class)} should accept
-     * those temporal types in the {@link Class} argument, but not all drivers support that.
-     *
-     * @see <a href="https://jcp.org/aboutJava/communityprocess/maintenance/jsr221/JDBC4.2MR-January2014.pdf">JDBC Maintenance Release 4.2</a>
+     * Whether the database supports {@code "GRANT USAGE ON SCHEMA …"}.
      */
-    static final int JAVA_TIME = 8;
+    public static final int GRANT_USAGE_ON_SCHEMA = 0x008;
+
+    /**
+     * Whether the database supports {@code "GRANT SELECT ON TABLE …"}.
+     */
+    public static final int GRANT_SELECT_ON_TABLE = 0x010;
+
+    /**
+     * Whether the database supports {@code "COMMENT ON …"}.
+     */
+    public static final int COMMENT = 0x020;
 
     /**
      * Whether the JDBC driver supports configuring readOnly mode on connection instances.
      * This feature is not supported in SQLite.
      */
-    static final int READ_ONLY_UPDATE = 16;
+    public static final int READ_ONLY_UPDATE = 0x040;
 
     /**
      * Whether the JDBC driver supports concurrent transactions.
      */
-    static final int CONCURRENCY = 32;
+    public static final int CONCURRENCY = 0x080;
+
+    /**
+     * Whether the JDBC driver supports conversions from objects to {@code java.time} API.
+     * The JDBC 4.2 specification provides a mapping from {@link java.sql.Types} to temporal objects.
+     * The specification suggests that {@link java.sql.ResultSet#getObject(int, Class)} should accept
+     * those temporal types in the {@link Class} argument, but not all drivers support that.
+     *
+     * @see <a href="https://jcp.org/aboutJava/communityprocess/maintenance/jsr221/JDBC4.2MR-January2014.pdf">JDBC Maintenance Release 4.2</a>
+     */
+    public static final int JAVA_TIME = 0x100;
 
     /**
      * Whether the spatial extension supports <abbr>SRID</abbr> in {@code ST_*} functions.
      */
-    static final int SRID = 64;
+    public static final int SRID = 0x200;
 
     /**
      * Do not allow instantiation of this class.
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TemporalDate.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TemporalDate.java
index 86aa1a6259..e54b06c085 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TemporalDate.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TemporalDate.java
@@ -240,7 +240,7 @@ public final class TemporalDate extends Date implements LenientComparable {
      */
     @Override
     public boolean equals(final Object other, final ComparisonMode mode) {
-        if (other instanceof TemporalDate && mode.ordinal() >= ComparisonMode.ALLOW_VARIANT.ordinal()) {
+        if (other instanceof TemporalDate && mode.allowsVariant()) {
             /*
              * The number of milliseconds may differ when comparing two local dates,
              * depending on which timezone was assumed when `Date` has been constructed.
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/TestDatabase.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/TestDatabase.java
index ceb470cc21..44354c5649 100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/TestDatabase.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/TestDatabase.java
@@ -310,7 +310,7 @@ public class TestDatabase implements AutoCloseable {
      * @throws ClassCastException if an element of the list is not a {@code String} or a {@code Supplier<InputStream>}.
      */
     public final void executeSQL(final List<?> scripts) throws IOException, SQLException {
-        try (Connection c = source.getConnection(); ScriptRunner r = new ScriptRunner(c, 1000)) {
+        try (Connection c = source.getConnection(); ScriptRunner r = new ScriptRunner(c, null, 1000)) {
             for (final Object sql : scripts) {
                 if (sql instanceof String s) {
                     r.run(s);
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/ScriptRunnerTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/ScriptRunnerTest.java
index 3507d656ae..4244019edc 100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/ScriptRunnerTest.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/ScriptRunnerTest.java
@@ -50,7 +50,7 @@ public final class ScriptRunnerTest extends TestCase {
         try (TestDatabase db = TestDatabase.create("ScriptRunner");
              Connection c = db.source.getConnection())
         {
-            final ScriptRunner sr = new ScriptRunner(c, 3);
+            final ScriptRunner sr = new ScriptRunner(c, null, 3);
             testSupportedFlags(sr);
             testRegularExpressions(sr);
         }
@@ -63,12 +63,7 @@ public final class ScriptRunnerTest extends TestCase {
      */
     @TestStep
     public static void testSupportedFlags(final ScriptRunner sr) {
-        assertFalse(sr.isCatalogSupported);
-        assertTrue (sr.isSchemaSupported);
-        assertFalse(sr.isGrantOnSchemaSupported);
-        assertFalse(sr.isGrantOnTableSupported);
         assertFalse(sr.isEnumTypeSupported);
-        assertFalse(sr.isCommentSupported);
     }
 
     /**
@@ -83,7 +78,7 @@ public final class ScriptRunnerTest extends TestCase {
         assertFalse(sr.isSupported("CREATE CAST (VARCHAR AS CI_DateTypeCode) WITH INOUT AS ASSIGNMENT"));
         assertTrue (sr.isSupported("CREATE TABLE CI_Citation (…)"));
         assertFalse(sr.isSupported("GRANT USAGE ON SCHEMA metadata TO PUBLIC"));
-        assertFalse(sr.isSupported("GRANT SELECT ON TABLE epsg_coordinatereferencesystem TO PUBLIC"));
+        assertFalse(sr.isSupported("GRANT SELECT ON TABLE \"Coordinate Reference System\" TO PUBLIC"));
         assertFalse(sr.isSupported("COMMENT ON SCHEMA metadata IS 'ISO 19115 metadata'"));
     }
 }
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK22.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SupportsTest.java
similarity index 50%
copy from endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK22.java
copy to endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SupportsTest.java
index adc7887cf7..18b2a33d45 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK22.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SupportsTest.java
@@ -14,31 +14,41 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.pending.jdk;
+package org.apache.sis.metadata.sql.privy;
 
-import java.io.Console;
+import java.lang.reflect.Field;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.test.TestCase;
 
 
 /**
- * Place holder for some functionalities defined in a JDK more recent than Java 11.
+ * Verifies the constants declared in {@link Supports}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-public final class JDK22 {
+public final class SupportsTest extends TestCase {
     /**
-     * Do not allow instantiation of this class.
+     * Creates a new test case.
      */
-    private JDK22() {
+    public SupportsTest() {
     }
 
     /**
-     * Placeholder for {@code Console.isTerminal()}.
+     * Verifies that there is no collision between the constant values.
+     *
+     * @throws IllegalAccessException if a field is not public.
      */
-    public static boolean isTerminal(Console c) {
-        try {
-            return (Boolean) Console.class.getMethod("isTerminal").invoke(c);
-        } catch (ReflectiveOperationException e) {
-            return true;
+    @Test
+    public void ensureNoCollision() throws IllegalAccessException {
+        int allFlags = 0;
+        for (Field field : Supports.class.getFields()) {
+            final int flag = field.getInt(null);
+            assertEquals(1, Integer.bitCount(flag));
+            assertNotEquals(allFlags, allFlags |= flag);
         }
+        assertNotEquals(0, allFlags);
     }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractSingleCRS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractSingleCRS.java
index 686ef5b11a..ac88426d4a 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractSingleCRS.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/AbstractSingleCRS.java
@@ -31,6 +31,7 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.cs.AbstractCS;
 import org.apache.sis.referencing.internal.Resources;
+import org.apache.sis.metadata.privy.Identifiers;
 import org.apache.sis.metadata.privy.ImplementationHelper;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -208,6 +209,22 @@ class AbstractSingleCRS<D extends Datum> extends AbstractCRS implements SingleCR
         return ensemble;
     }
 
+    /**
+     * Returns whether the given datum may be considered as equivalent to the given datum ensemble.
+     * Used for comparisons with {@link ComparisonMode#APPROXIMATE} for interoperability between
+     * the legacy and the new definition of EPSG:4326.
+     */
+    private static boolean isHeuristicMatchForName(final DatumEnsemble<?> ensemble, final Datum datum, final ComparisonMode mode) {
+        if (ensemble == null || datum == null) {
+            return false;
+        }
+        final Boolean match = Identifiers.hasCommonIdentifier(ensemble.getIdentifiers(), datum.getIdentifiers());
+        if (match != null) {
+            return match;
+        }
+        return IdentifiedObjects.isHeuristicMatchForName(datum, ensemble.getName().getCode());
+    }
+
     /**
      * Compares this coordinate reference system with the specified object for equality.
      *
@@ -218,7 +235,7 @@ class AbstractSingleCRS<D extends Datum> extends AbstractCRS implements SingleCR
      * @hidden because nothing new to said.
      */
     @Override
-    public boolean equals(final Object object, final ComparisonMode mode) {
+    public boolean equals(final Object object, ComparisonMode mode) {
         if (super.equals(object, mode)) {
             switch (mode) {
                 case STRICT: {
@@ -227,8 +244,17 @@ class AbstractSingleCRS<D extends Datum> extends AbstractCRS implements SingleCR
                 }
                 default: {
                     final var that = (SingleCRS) object;
-                    return Utilities.deepEquals(getDatum(), that.getDatum(), mode) &&
-                           Utilities.deepEquals(getDatumEnsemble(), that.getDatumEnsemble(), mode);
+                    final var d1   = this.getDatum();
+                    final var d2   = that.getDatum();
+                    if (mode == ComparisonMode.DEBUG) {
+                        mode = ComparisonMode.ALLOW_VARIANT;    // For avoiding too early `AssertionError`.
+                    }
+                    if (Utilities.deepEquals(d1, d2, mode)) {
+                        return mode.allowsVariant() || Utilities.deepEquals(getDatumEnsemble(), that.getDatumEnsemble(), mode);
+                    } else if (mode.allowsVariant()) {
+                        return isHeuristicMatchForName(this.getDatumEnsemble(), d2, mode) ||
+                               isHeuristicMatchForName(that.getDatumEnsemble(), d1, mode);
+                    }
                 }
             }
         }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java
index ffd308007d..feba4754fe 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java
@@ -586,7 +586,7 @@ next:   for (final CoordinateSystemAxis axis : axes) {
                 if (dimension != that.getDimension()) {
                     return false;
                 }
-                if (mode != ComparisonMode.ALLOW_VARIANT) {
+                if (!mode.allowsVariant()) {
                     for (int i=0; i<dimension; i++) {
                         if (!Utilities.deepEquals(getAxis(i), that.getAxis(i), mode)) {
                             return false;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/AbstractDatum.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/AbstractDatum.java
index c473e4e1d3..17be60c6da 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/AbstractDatum.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/AbstractDatum.java
@@ -451,7 +451,7 @@ public class AbstractDatum extends AbstractIdentifiedObject implements Datum {
                  * and parameters. We extend this rule to datum as well.
                  */
                 final var that = (Datum) object;
-                if (!compareDynamicReferenceFrames(that, mode)) {
+                if (!(mode.allowsVariant() || compareDynamicReferenceFrames(that, mode))) {
                     return false;
                 }
                 final Boolean match = Identifiers.hasCommonIdentifier(getIdentifiers(), that.getIdentifiers());
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 5a7eac4e08..27b01d3b7d 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
@@ -134,7 +134,7 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali
         final int columnNameStart = buffer.append("SELECT ").length();
         final int columnNameEnd = buffer.append(table.codeColumn).length();
         buffer.append(" FROM ").append(table.table);
-        final Class<?> tableType = table.where(type, buffer);
+        final Class<?> tableType = table.where(factory, type, buffer);
         final int conditionStart = buffer.length();
         if (table.showColumn != null) {
             buffer.append(table.showColumn).append("<>0 AND ");
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 47975c6b37..cab3292126 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
@@ -418,7 +418,7 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, object)) {
          * It may be absent (typically, only datums or reference frames have that condition).
          */
         buffer.append("SELECT ").append(table.codeColumn).append(" FROM ").append(table.table);
-        table.where(object, buffer);                // Unconditionally append a "WHERE" clause.
+        table.where(dao, object, buffer);           // Unconditionally append a "WHERE" clause.
         boolean isNext = false;
         for (final Condition filter : filters) {
             isNext |= filter.appendToWhere(buffer, isNext);
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 59997d0496..49eca3b051 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
@@ -1596,7 +1596,7 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation",
                  */
                 final FactoryCall<CRSFactory, CoordinateReferenceSystem> constructor;
                 /*
-                 * The following switch statement should have a case for all "epsg_crs_kind" values enumerated
+                 * The following switch statement should have a case for all "CRS Kind" values enumerated
                  * in the "EPSG_Prepare.sql" file, except that the values in this Java code are in lower cases.
                  */
                 switch (type.toLowerCase(Locale.US)) {
@@ -1918,7 +1918,7 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation",
                  */
                 final FactoryCall<DatumFactory, ? extends Datum> constructor;
                 /*
-                 * The following switch statement should have a case for all "epsg_datum_kind" values enumerated
+                 * The following switch statement should have a case for all "Datum Kind" values enumerated
                  * in the "EPSG_Prepare.sql" file, except that the values in this Java code are in lower cases.
                  */
                 switch (type.toLowerCase(Locale.US)) {
@@ -2009,9 +2009,9 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation",
     {
         double max = Double.NaN;
         try (ResultSet result = executeQueryForCodes(
-                "DatumEnsemble",
+                "Datum Ensemble",
                 "SELECT ENSEMBLE_ACCURACY"
-                        + " FROM \"DatumEnsemble\""
+                        + " FROM \"Datum Ensemble\""
                         + " WHERE DATUM_ENSEMBLE_CODE = ?", code))
         {
             // Should have exactly one value. The loop is a paranoiac safety.
@@ -2025,9 +2025,9 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation",
         final var accuracy = PositionalAccuracyConstant.ensemble(max);
         final List<Datum> members = createComponents(
                 GeodeticAuthorityFactory::createDatum,
-                "DatumEnsembleMember",
+                "Datum Ensemble Member",
                 "SELECT DATUM_CODE"
-                        + " FROM \"DatumEnsembleMember\""
+                        + " FROM \"Datum Ensemble Member\""
                         + " WHERE DATUM_ENSEMBLE_CODE = ?"
                         + " ORDER BY DATUM_SEQUENCE", code);
         return (factory, metadata) -> DefaultDatumEnsemble.castOrCopy(factory.createDatumEnsemble(metadata, members, accuracy));
@@ -2104,12 +2104,12 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation",
         var returnValue = (IdentifiedObject) localCache.get(cacheKey);
         if (returnValue == null) {
             try (ResultSet result = executeQueryForCodes(
-                    "ConventionalRS",
+                    "Conventional RS",
                     "SELECT"+ /* column 1 */ " CONVENTIONAL_RS_CODE,"
                             + /* column 2 */ " CONVENTIONAL_RS_NAME,"
                             + /* column 3 */ " REMARKS,"
                             + /* column 4 */ " DEPRECATED"
-                            + " FROM \"ConventionalRS\""
+                            + " FROM \"Conventional RS\""
                             + " WHERE CONVENTIONAL_RS_CODE = ?", code))
             {
                 while (result.next()) {
@@ -2123,7 +2123,7 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation",
                      */
                     @SuppressWarnings("LocalVariableHidesMemberVariable")
                     final Map<String,Object> properties = createProperties(
-                            "ConventionalRS", epsg, name, null, null, null, remarks, deprecated);
+                            "Conventional RS", epsg, name, null, null, null, remarks, deprecated);
                     returnValue = ensureSingleton(new AbstractIdentifiedObject(properties), returnValue, code);
                     if (result.isClosed()) break;   // See createProperties(…) for explanation.
                 }
@@ -2506,7 +2506,7 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation",
                 final Map<String,Object> properties = createProperties(
                         "Coordinate System", epsg, name, null, null, null, remarks, deprecated);
                 /*
-                 * The following switch statement should have a case for all "epsg_cs_kind" values enumerated
+                 * The following switch statement should have a case for all "CS Kind" values enumerated
                  * in the "EPSG_Prepare.sql" file, except that the values in this Java code are in lower cases.
                  */
                 final CSFactory csFactory = owner.csFactory;
@@ -2723,9 +2723,9 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation",
         var returnValue = (RealizationMethod) localCache.get(cacheKey);
         if (returnValue == null && code != null) {
             try (ResultSet result = executeQueryForCodes(
-                    "DatumRealizationMethod",
+                    "Datum Realization Method",
                     "SELECT REALIZATION_METHOD_NAME"
-                            + " FROM \"DatumRealizationMethod\""
+                            + " FROM \"Datum Realization Method\""
                             + " WHERE REALIZATION_METHOD_CODE = ?", code))
             {
                 while (result.next()) {
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGFactory.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGFactory.java
index b01ecc5512..4c8dd3ac1b 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGFactory.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGFactory.java
@@ -19,7 +19,6 @@ package org.apache.sis.referencing.factory.sql;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
-import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
@@ -146,8 +145,7 @@ public class EPSGFactory extends ConcurrentAuthorityFactory<EPSGDataAccess> impl
     /**
      * The name of the schema that contains the EPSG tables, or {@code null} or an empty string.
      * <ul>
-     *   <li>The {@code ""} value retrieves the EPSG tables without a schema.
-     *       In such case, table names are prefixed by {@value SQLTranslator#TABLE_PREFIX}.</li>
+     *   <li>The {@code ""} value retrieves the EPSG tables without a schema.</li>
      *   <li>The {@code null} value means that the schema name should not be used to narrow the search.
      *       In such case, {@link SQLTranslator} will tries to automatically detect the schema.</li>
      * </ul>
@@ -354,8 +352,8 @@ public class EPSGFactory extends ConcurrentAuthorityFactory<EPSGDataAccess> impl
      *     If no provider is specified, then this method searches on the module path (with {@link java.util.ServiceLoader})
      *     for user-provided implementations of {@code InstallationScriptProvider}.
      *     If no user-specified provider is found, then this method will search for
-     *     {@code "EPSG_*Tables.sql"}, {@code "EPSG_*Data.sql"} and {@code "EPSG_*FKeys.sql"} files in the
-     *     {@code $SIS_DATA/Databases/ExternalSources} directory where {@code *} stands for any characters
+     *     {@code "*Tables*.sql"}, {@code "*Data*.sql"} and {@code "*FKeys*.sql"} files in the
+     *     {@code $SIS_DATA/Databases/ExternalSources/EPSG} directory where {@code *} stands for any characters
      *     provided that there is no ambiguity.</li>
      * </ul>
      *
@@ -378,36 +376,38 @@ public class EPSGFactory extends ConcurrentAuthorityFactory<EPSGDataAccess> impl
         String    message = null;
         Exception failure = null;
         boolean   success = false;
-        try (EPSGInstaller installer = new EPSGInstaller(Objects.requireNonNull(connection))) {
-            final boolean ac = connection.getAutoCommit();
-            if (ac) {
+        try {
+            if (catalog != null) {
+                connection.setCatalog(catalog);
+            }
+            final boolean autoCommit = connection.getAutoCommit();
+            if (autoCommit) {
                 connection.setAutoCommit(false);
             }
-            try {
+            try (EPSGInstaller installer = new EPSGInstaller(connection, schema)) {
                 try {
-                    if (!"".equals(schema)) {                                           // Schema may be null.
-                        installer.setSchema(schema != null ? schema : Constants.EPSG);
-                        if (catalog != null && !catalog.isEmpty()) {
-                            installer.prependNamespace(catalog);
-                        }
-                    }
                     success = installer.run(scriptProvider, locale);
-                } finally {
-                    if (ac) {
-                        if (success) {
-                            connection.commit();
-                        } else {
-                            connection.rollback();
-                        }
-                        connection.setAutoCommit(true);
+                } catch (IOException | SQLException e) {
+                    message = installer.failure(locale);
+                    failure = e;
+                }
+            } finally {
+                if (autoCommit) {
+                    if (success) {
+                        connection.commit();
+                    } else {
+                        connection.rollback();
                     }
+                    connection.setAutoCommit(true);
                 }
-            } catch (IOException | SQLException e) {
-                message = installer.failure(locale);
-                failure = e;
             }
         } catch (SQLException e) {
-            failure = e;
+            if (failure != null) {
+                failure.addSuppressed(e);
+            } else {
+                failure = e;
+            }
+            success = false;
         }
         if (!success) {
             if (message == null) {
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 124eb96682..28afba7e4b 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
@@ -21,14 +21,11 @@ import java.io.IOException;
 import java.io.FileNotFoundException;
 import java.io.BufferedReader;
 import java.sql.Connection;
-import java.sql.DatabaseMetaData;
 import java.sql.SQLException;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
-import org.apache.sis.util.ArraysExt;
-import org.apache.sis.util.StringBuilders;
 import org.apache.sis.util.privy.Constants;
-import static org.apache.sis.util.privy.Constants.EPSG;
 import org.apache.sis.util.resources.Messages;
 import org.apache.sis.util.logging.PerformanceLevel;
 import org.apache.sis.metadata.sql.privy.ScriptRunner;
@@ -47,142 +44,44 @@ import org.apache.sis.setup.InstallationResources;
  */
 final class EPSGInstaller extends ScriptRunner {
     /**
-     * The pattern for an {@code "UPDATE … SET … REPLACE"} instruction.
-     * Example:
-     *
-     * {@snippet lang="sql" :
-     *     UPDATE epsg_datum
-     *     SET datum_name = replace(datum_name, CHAR(182), CHAR(10));
-     *     }
-     *
-     * Note: this regular expression uses a capturing group.
+     * The quoted identifiers to replace, or an empty map if none.
+     * Used for replacing enumeration types when not supported by the target database.
      */
-    static final String REPLACE_STATEMENT =
-            "UPDATE\\s+[\\w\\.\" ]+\\s+SET\\s+(\\w+)\\s*=\\s*replace\\s*\\(\\s*\\1\\W+.*";
+    private final Map<String, String> identifierReplacements;
 
     /**
-     * {@code true} if the Pilcrow character (¶ - decimal code 182) should be replaced by Line Feed
-     * (LF - decimal code 10). This is a possible workaround when the database does not support the
-     * {@code REPLACE(column, CHAR(182), CHAR(10))} SQL statement, but accepts LF.
+     * Whether to apply the replacements in the {@link #identifierReplacements} map.
      */
-    private final boolean replacePilcrow;
+    private boolean applyReplacements;
 
     /**
      * Creates a new runner which will execute the statements using the given connection.
-     * The encoding is {@code "ISO-8859-1"}, which is the encoding used for the files provided by EPSG.
+     * This constructor creates immediately a schema of the given name in the database.
      *
      * @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.
      */
-    public EPSGInstaller(final Connection connection) throws SQLException {
-        super(connection, 100);
-        final DatabaseMetaData metadata = connection.getMetaData();
-        final String[] functions = metadata.getStringFunctions().split("\\s*,\\s*");
-        if (!ArraysExt.containsIgnoreCase(functions, "REPLACE")) {
-            addStatementToSkip(REPLACE_STATEMENT);
+    public EPSGInstaller(final Connection connection, final String schema) throws SQLException {
+        super(connection, schema == null ? Constants.EPSG : schema.isEmpty() ? null : schema, 100);
+        if (isEnumTypeSupported) {
+            identifierReplacements = Map.of();
+        } else {
+            identifierReplacements = Map.of(
+                    "Datum Kind",        "VARCHAR(16)",    // Original: VARCHAR(24) for column "datum_type".
+                    "CRS Kind",          "VARCHAR(13)",    // Original: VARCHAR(24) for column "coord_ref_sys_kind".
+                    "CS Kind",           "VARCHAR(15)",    // Original: VARCHAR(24) for column "coord_sys_type".
+                    "Supersession Type", "VARCHAR(12)",    // Original: VARCHAR(50) for column "supersession_type".
+                    "Table Name",     ENUM_REPLACEMENT);   // Original: VARCHAR(80) for columns "object_table_name".
         }
-        /*
-         * The SQL scripts provided by EPSG contains some lines with only a "COMMIT" statement.
-         * This statement is not understood by all databases, and interferes with our calls to
-         * setAutoCommit(false) ... commit() / rollback().
-         */
-        addStatementToSkip("COMMIT");
-        replacePilcrow = false;         // Never supported for now.
     }
 
     /**
-     * Creates immediately a schema of the given name in the database.
-     * The {@code "epsg_"} prefix in table names or the {@code "epsg"}
-     * schema name will be replaced by the given schema name.
-     *
-     * <p>This method should be invoked only once. It does nothing if the database does not supports schema.</p>
-     *
-     * @param  schema  the schema (usually {@code "epsg"}).
-     * @throws SQLException if the schema cannot be created.
-     * @throws IOException if an I/O operation was required and failed.
+     * The <abbr>SQL</abbr> type to use as a replacement for enumerated values in databases that do not
+     * support enumerations. The maximal length declared in this constant should be the greatest length
+     * declared in {@code VARCHAR(…)} substitutions done when {@link #isEnumTypeSupported} is false.
      */
-    public void setSchema(final String schema) throws SQLException, IOException {
-        if (isSchemaSupported) {
-            /*
-             * Creates the schema on the database.
-             * Note that we do not quote the schema name, which is a somewhat arbitrary choice.
-             */
-            final var sb = new StringBuilder(40);
-            execute(sb.append("CREATE SCHEMA ").append(identifierQuote).append(schema).append(identifierQuote));
-            if (isGrantOnSchemaSupported) {
-                sb.setLength(0);
-                execute(sb.append("GRANT USAGE ON SCHEMA ")
-                        .append(identifierQuote).append(schema).append(identifierQuote).append(" TO ").append(PUBLIC));
-            }
-            /*
-             * Mapping from the table names used in the SQL scripts to the names used in `EPSGDataAccess`.
-             * Those names are the ones that were used in the original EPSG dataset in MS-Access database.
-             * We use those original names because they are easier to read than the names in SQL scripts.
-             *
-             * TODO: move to org.apache.sis.referencing.factory.sql.epsg.DataScriptFormatter.
-             */
-            addReplacement(SQLTranslator.TABLE_PREFIX + "alias",                      "Alias");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "area",                       "Area");      // Deprecated (removed in EPSG 10)
-            addReplacement(SQLTranslator.TABLE_PREFIX + "change",                     "Change");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "conventionalrs",             "ConventionalRS");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordinateaxis",             "Coordinate Axis");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordinateaxisname",         "Coordinate Axis Name");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordoperation",             "Coordinate_Operation");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordoperationmethod",       "Coordinate_Operation Method");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordoperationparam",        "Coordinate_Operation Parameter");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordoperationparamusage",   "Coordinate_Operation Parameter Usage");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordoperationparamvalue",   "Coordinate_Operation Parameter Value");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordoperationpath",         "Coordinate_Operation Path");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordinatereferencesystem",  "Coordinate Reference System");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "coordinatesystem",           "Coordinate System");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "datum",                      "Datum");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "datumensemble",              "DatumEnsemble");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "datumensemblemember",        "DatumEnsembleMember");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "datumrealizationmethod",     "DatumRealizationMethod");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "definingoperation",          "DefiningOperation");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "deprecation",                "Deprecation");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "ellipsoid",                  "Ellipsoid");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "extent",                     "Extent");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "namingsystem",               "Naming System");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "primemeridian",              "Prime Meridian");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "scope",                      "Scope");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "supersession",               "Supersession");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "unitofmeasure",              "Unit of Measure");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "usage",                      "Usage");
-            addReplacement(SQLTranslator.TABLE_PREFIX + "versionhistory",             "Version History");
-            if (isEnumTypeSupported) {
-                addReplacement(SQLTranslator.TABLE_PREFIX + "datum_kind",             "Datum Kind");
-                addReplacement(SQLTranslator.TABLE_PREFIX + "crs_kind",               "CRS Kind");
-                addReplacement(SQLTranslator.TABLE_PREFIX + "cs_kind",                "CS Kind");
-                addReplacement(SQLTranslator.TABLE_PREFIX + "table_name",             "Table Name");
-            }
-            prependNamespace(schema);
-        }
-        if (!isEnumTypeSupported) {
-            addReplacement(SQLTranslator.TABLE_PREFIX + "datum_kind", TableInfo.ENUM_REPLACEMENT);
-            addReplacement(SQLTranslator.TABLE_PREFIX + "crs_kind",   TableInfo.ENUM_REPLACEMENT);
-            addReplacement(SQLTranslator.TABLE_PREFIX + "cs_kind",    TableInfo.ENUM_REPLACEMENT);
-            addReplacement(SQLTranslator.TABLE_PREFIX + "table_name", TableInfo.ENUM_REPLACEMENT);
-        }
-    }
-
-    /**
-     * Prepends the given schema or catalog to all table names.
-     */
-    final void prependNamespace(final String schema) {
-        modifyReplacements((key, value) -> {
-            if (key.startsWith(SQLTranslator.TABLE_PREFIX)) {
-                final var buffer = new StringBuilder(value.length() + schema.length() + 5);
-                buffer.append(identifierQuote).append(schema).append(identifierQuote).append('.');
-                final boolean isQuoted = value.endsWith(identifierQuote);
-                if (!isQuoted) buffer.append(identifierQuote);
-                buffer.append(value);
-                if (!isQuoted) buffer.append(identifierQuote);
-                value = buffer.toString();
-            }
-            return value;
-        });
-    }
+    static final String ENUM_REPLACEMENT = "VARCHAR(36)";
 
     /**
      * Invoked for each text found in a SQL statement. This method replaces {@code ''} by {@code Null}.
@@ -190,7 +89,7 @@ final class EPSGInstaller extends ScriptRunner {
      * same as "information is an empty string". This replacement is okay in this particular case
      * since there is no field in the EPSG database for which we really want an empty string.
      *
-     * @param sql    the whole SQL statement.
+     * @param sql    the whole <abbr>SQL</abbr> statement.
      * @param lower  index of the first character of the text in {@code sql}.
      * @param upper  index after the last character of the text in {@code sql}.
      */
@@ -201,11 +100,8 @@ final class EPSGInstaller extends ScriptRunner {
             default: {
                 return;
             }
-            /*
-             * Replace '' by Null for every table.
-             */
             case 2: {
-                replacement = "Null";
+                replacement = "Null";   // Replace '' by Null for every table.
                 break;
             }
         }
@@ -213,17 +109,21 @@ final class EPSGInstaller extends ScriptRunner {
     }
 
     /**
-     * Modifies the SQL statement before to execute it, or omit unsupported statements.
+     * Replaces an enumeration type by a standard type if enumerations are not supported.
      *
-     * @throws SQLException if an error occurred while executing the SQL statement.
-     * @throws IOException  if an I/O operation was required and failed.
+     * @param sql    the whole <abbr>SQL</abbr> statement.
+     * @param lower  index of the first character of the identifier in {@code sql}.
+     * @param upper  index after the last character of the identifier in {@code sql}.
      */
     @Override
-    protected int execute(final StringBuilder sql) throws SQLException, IOException {
-        if (replacePilcrow) {
-            StringBuilders.replace(sql, "¶", "\n");
+    protected void editQuotedIdentifier(final StringBuilder sql, final int lower, final int upper) {
+        if (applyReplacements) {
+            final int n = identifierQuote.length();
+            final String r = identifierReplacements.get(sql.substring(lower + n, upper - n));
+            if (r != null) {
+                sql.replace(lower, upper, r);   // This replacement removes the quotes.
+            }
         }
-        return super.execute(sql);
     }
 
     /**
@@ -247,13 +147,15 @@ final class EPSGInstaller extends ScriptRunner {
         InstallationScriptProvider.log(Messages.forLocale(locale).createLogRecord(
                 Level.INFO,
                 Messages.Keys.CreatingSchema_2,
-                EPSG,
+                Constants.EPSG,
                 SQLUtilities.getSimplifiedURL(getConnection().getMetaData())));
-        final String[] scripts = scriptProvider.getResourceNames(EPSG);
+        final String[] scripts = scriptProvider.getResourceNames(Constants.EPSG);
         int numRows = 0;
         for (int i=0; i<scripts.length; i++) {
-            try (BufferedReader in = scriptProvider.openScript(EPSG, i)) {
-                numRows += run(scripts[i], in);
+            final String script = scripts[i];
+            applyReplacements = !(identifierReplacements.isEmpty() || InstallationScriptProvider.PREPARE.equals(script));
+            try (BufferedReader in = scriptProvider.openScript(Constants.EPSG, i)) {
+                numRows += run(script, in);
             }
         }
         time = System.nanoTime() - time;
@@ -273,7 +175,7 @@ final class EPSGInstaller extends ScriptRunner {
      *   <li>A provider from a publicly supported dependency such as {@code sis-epsg.jar} or {@code sis-embedded.jar}.
      *       Users have to put one of those dependencies in the module path themselves. This action is interpreted as
      *       an acceptance of EPSG terms of use, so no license agreement window will popup.</li>
-     *   <li>A provider using the SQL scripts in the {@code $SIS_DATA/Databases/ExternalSources} directory.
+     *   <li>A provider using the SQL scripts in the {@code $SIS_DATA/Databases/ExternalSources/EPSG} directory.
      *       Users have to put those scripts in that directory manually. This action is interpreted as an
      *       acceptance of EPSG terms of use, so no license agreement window will popup.</li>
      *   <li>A provider offering users to automatically download the data. Those providers are defined by
@@ -287,7 +189,7 @@ final class EPSGInstaller extends ScriptRunner {
     private static InstallationResources lookupProvider(final Locale locale) throws IOException {
         InstallationResources fallback = null;
         for (final InstallationResources provider : InstallationResources.load()) {
-            if (provider.getAuthorities().contains(EPSG)) {
+            if (provider.getAuthorities().contains(Constants.EPSG)) {
                 if (!provider.getClass().isAnnotationPresent(Fallback.class)) {
                     return provider;
                 }
@@ -299,7 +201,7 @@ final class EPSGInstaller extends ScriptRunner {
          * we may fallback on a provider offering to download the data (those fallbacks are provided
          * by `org.apache.sis.console` and `org.apache.sis.gui` modules). Those fallbacks will ask to
          * the user if (s)he accepts EPSG terms of use. But before to use those fallbacks, check if the
-         * data have not been downloaded manually in the "$SIS_DATA/Databases/ExternalSources" directory.
+         * data have not been downloaded manually in the "$SIS_DATA/Databases/ExternalSources/EPSG" directory.
          */
         final var manual = new InstallationScriptProvider.Default(locale);
         return manual.getAuthorities().isEmpty() ? fallback : manual;
@@ -311,7 +213,7 @@ final class EPSGInstaller extends ScriptRunner {
      * error occurred, if such information is available.
      */
     final String failure(final Locale locale) {
-        String message = Messages.forLocale(locale).getString(Messages.Keys.CanNotCreateSchema_1, EPSG);
+        String message = Messages.forLocale(locale).getString(Messages.Keys.CanNotCreateSchema_1, Constants.EPSG);
         String status = status(locale);
         if (status != null) {
             message = message + ' ' + status;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Finish.sql b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Finish.sql
index 2c28d81c73..b649bf65ce 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Finish.sql
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Finish.sql
@@ -12,10 +12,10 @@
 --
 
 -- "Scale factor at natural origin" (EPSG:8805) shall be dimensionless (EPSG:9201), not in "m" units (EPSG:9001).
-UPDATE epsg_coordoperationparamvalue SET uom_code = 9201 WHERE parameter_code = 8805 AND uom_code = 9001;
+UPDATE "Coordinate_Operation Parameter Value" SET uom_code = 9201 WHERE parameter_code = 8805 AND uom_code = 9001;
 
 -- Value 44°87′ is illegal for DMS units (EPSG:9110). A value equivalent to the erroneous DMS value is 45°27′.
-UPDATE epsg_coordoperationparamvalue SET parameter_value = 45.27 WHERE parameter_value = 44.87 AND uom_code = 9110;
+UPDATE "Coordinate_Operation Parameter Value" SET parameter_value = 45.27 WHERE parameter_value = 44.87 AND uom_code = 9110;
 
 
 
@@ -29,77 +29,84 @@ UPDATE epsg_coordoperationparamvalue SET parameter_value = 45.27 WHERE parameter
 -- Indexes for queries used by EPSGDataAccess.createFoo(epsgcode) methods. --
 -- Indexed fields are numeric values used mainly in equality comparisons.  --
 -----------------------------------------------------------------------------
-CREATE INDEX ix_alias_object_code                ON epsg_alias                     (object_code);
-CREATE INDEX ix_crs_datum_code                   ON epsg_coordinatereferencesystem (datum_code);
-CREATE INDEX ix_crs_projection_code              ON epsg_coordinatereferencesystem (projection_conv_code);
-CREATE INDEX ix_coordinate_axis_code             ON epsg_coordinateaxis            (coord_axis_code);
-CREATE INDEX ix_coordinate_axis_sys_code         ON epsg_coordinateaxis            (coord_sys_code);
-CREATE INDEX ix_coordinate_operation_crs         ON epsg_coordoperation            (source_crs_code, target_crs_code);
-CREATE INDEX ix_coordinate_operation_method_code ON epsg_coordoperation            (coord_op_method_code);
-CREATE INDEX ix_parameter_usage_method_code      ON epsg_coordoperationparamusage  (coord_op_method_code);
-CREATE INDEX ix_parameter_values                 ON epsg_coordoperationparamvalue  (coord_op_code, coord_op_method_code);
-CREATE INDEX ix_parameter_value_code             ON epsg_coordoperationparamvalue  (parameter_code);
-CREATE INDEX ix_path_concat_operation_code       ON epsg_coordoperationpath        (concat_operation_code);
-CREATE INDEX ix_supersession_object_code         ON epsg_supersession              (object_code);
+CREATE INDEX ix_alias_object_code                ON "Alias"                                (object_code);
+CREATE INDEX ix_crs_datum_code                   ON "Coordinate Reference System"          (datum_code);
+CREATE INDEX ix_crs_projection_code              ON "Coordinate Reference System"          (projection_conv_code);
+CREATE INDEX ix_coordinate_axis_code             ON "Coordinate Axis"                      (coord_axis_code);
+CREATE INDEX ix_coordinate_axis_sys_code         ON "Coordinate Axis"                      (coord_sys_code);
+CREATE INDEX ix_coordinate_operation_crs         ON "Coordinate_Operation"                 (source_crs_code, target_crs_code);
+CREATE INDEX ix_coordinate_operation_method_code ON "Coordinate_Operation"                 (coord_op_method_code);
+CREATE INDEX ix_parameter_usage_method_code      ON "Coordinate_Operation Parameter Usage" (coord_op_method_code);
+CREATE INDEX ix_parameter_values                 ON "Coordinate_Operation Parameter Value" (coord_op_code, coord_op_method_code);
+CREATE INDEX ix_parameter_value_code             ON "Coordinate_Operation Parameter Value" (parameter_code);
+CREATE INDEX ix_path_concat_operation_code       ON "Coordinate_Operation Path"            (concat_operation_code);
+CREATE INDEX ix_supersession_object_code         ON "Supersession"                         (object_code);
 
 
 -----------------------------------------------------------------------------
 -- Indexes for queries used by EPSGDataAccess.createFoo(epsgcode) methods. --
 -- Indexed fields are numeric values used in ORDER BY clauses.             --
 -----------------------------------------------------------------------------
-CREATE INDEX ix_coordinate_axis_order            ON epsg_coordinateaxis           (coord_axis_order);
-CREATE INDEX ix_coordinate_operation_accuracy    ON epsg_coordoperation           (coord_op_accuracy);
-CREATE INDEX ix_parameter_order                  ON epsg_coordoperationparamusage (sort_order);
-CREATE INDEX ix_path_concat_operation_step       ON epsg_coordoperationpath       (op_path_step);
-CREATE INDEX ix_supersession_object_year         ON epsg_supersession             (supersession_year);
-CREATE INDEX ix_version_history_date             ON epsg_versionhistory           (version_date);
+CREATE INDEX ix_coordinate_axis_order            ON "Coordinate Axis"                      (coord_axis_order);
+CREATE INDEX ix_coordinate_operation_accuracy    ON "Coordinate_Operation"                 (coord_op_accuracy);
+CREATE INDEX ix_parameter_order                  ON "Coordinate_Operation Parameter Usage" (sort_order);
+CREATE INDEX ix_path_concat_operation_step       ON "Coordinate_Operation Path"            (op_path_step);
+CREATE INDEX ix_datum_member_order               ON "Datum Ensemble Member"                (datum_sequence);
+CREATE INDEX ix_supersession_object_year         ON "Supersession"                         (supersession_year);
+CREATE INDEX ix_version_history_date             ON "Version History"                      (version_date);
 
 
 -----------------------------------------------------------------------------
 -- Indexes on the object names for finding an EPSG code from a name.       --
 -----------------------------------------------------------------------------
-CREATE INDEX ix_name_crs            ON epsg_coordinatereferencesystem (coord_ref_sys_name);
-CREATE INDEX ix_name_cs             ON epsg_coordinatesystem          (coord_sys_name);
-CREATE INDEX ix_name_axis           ON epsg_coordinateaxisname        (coord_axis_name);
-CREATE INDEX ix_name_datum          ON epsg_datum                     (datum_name);
-CREATE INDEX ix_name_ellipsoid      ON epsg_ellipsoid                 (ellipsoid_name);
-CREATE INDEX ix_name_prime_meridian ON epsg_primemeridian             (prime_meridian_name);
-CREATE INDEX ix_name_coord_op       ON epsg_coordoperation            (coord_op_name);
-CREATE INDEX ix_name_method         ON epsg_coordoperationmethod      (coord_op_method_name);
-CREATE INDEX ix_name_parameter      ON epsg_coordoperationparam       (parameter_name);
-CREATE INDEX ix_name_unit           ON epsg_unitofmeasure             (unit_of_meas_name);
-CREATE INDEX ix_alias               ON epsg_alias                     (object_table_name, alias);
+CREATE INDEX ix_name_crs            ON "Coordinate Reference System"    (coord_ref_sys_name);
+CREATE INDEX ix_name_cs             ON "Coordinate System"              (coord_sys_name);
+CREATE INDEX ix_name_axis           ON "Coordinate Axis Name"           (coord_axis_name);
+CREATE INDEX ix_name_datum          ON "Datum"                          (datum_name);
+CREATE INDEX ix_name_ellipsoid      ON "Ellipsoid"                      (ellipsoid_name);
+CREATE INDEX ix_name_prime_meridian ON "Prime Meridian"                 (prime_meridian_name);
+CREATE INDEX ix_name_coord_op       ON "Coordinate_Operation"           (coord_op_name);
+CREATE INDEX ix_name_method         ON "Coordinate_Operation Method"    (coord_op_method_name);
+CREATE INDEX ix_name_parameter      ON "Coordinate_Operation Parameter" (parameter_name);
+CREATE INDEX ix_name_unit           ON "Unit of Measure"                (unit_of_meas_name);
+CREATE INDEX ix_alias               ON "Alias"                          (object_table_name, alias);
 
 
 -----------------------------------------------------------------------------
 -- Indexes used by EPSGDataAccess.Finder for reverse operation.            --
 -----------------------------------------------------------------------------
-CREATE INDEX ix_major_ellipsoid ON epsg_ellipsoid                 (semi_major_axis);
-CREATE INDEX ix_geogcrs_crs     ON epsg_coordinatereferencesystem (base_crs_code);
-CREATE INDEX ix_ellipsoid_datum ON epsg_datum                     (ellipsoid_code);
+CREATE INDEX ix_major_ellipsoid ON "Ellipsoid"                   (semi_major_axis);
+CREATE INDEX ix_geogcrs_crs     ON "Coordinate Reference System" (base_crs_code);
+CREATE INDEX ix_ellipsoid_datum ON "Datum"                       (ellipsoid_code);
 
 
 -----------------------------------------------------------------------------
 -- Grants read permission to all tables in the EPSG schema.                --
 -----------------------------------------------------------------------------
-GRANT SELECT ON TABLE epsg_alias TO PUBLIC;
-GRANT SELECT ON TABLE epsg_area TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordinateaxis TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordinateaxisname TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordoperation TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordoperationmethod TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordoperationparam TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordoperationparamusage TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordoperationparamvalue TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordoperationpath TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordinatereferencesystem TO PUBLIC;
-GRANT SELECT ON TABLE epsg_coordinatesystem TO PUBLIC;
-GRANT SELECT ON TABLE epsg_datum TO PUBLIC;
-GRANT SELECT ON TABLE epsg_ellipsoid TO PUBLIC;
-GRANT SELECT ON TABLE epsg_namingsystem TO PUBLIC;
-GRANT SELECT ON TABLE epsg_primemeridian TO PUBLIC;
-GRANT SELECT ON TABLE epsg_supersession TO PUBLIC;
-GRANT SELECT ON TABLE epsg_unitofmeasure TO PUBLIC;
-GRANT SELECT ON TABLE epsg_versionhistory TO PUBLIC;
-GRANT SELECT ON TABLE epsg_change TO PUBLIC;
-GRANT SELECT ON TABLE epsg_deprecation TO PUBLIC;
+GRANT SELECT ON TABLE "Alias" TO PUBLIC;
+GRANT SELECT ON TABLE "Conventional RS" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate Axis" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate Axis Name" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate Reference System" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate System" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate_Operation" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate_Operation Method" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate_Operation Parameter" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate_Operation Parameter Usage" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate_Operation Parameter Value" TO PUBLIC;
+GRANT SELECT ON TABLE "Coordinate_Operation Path" TO PUBLIC;
+GRANT SELECT ON TABLE "Datum" TO PUBLIC;
+GRANT SELECT ON TABLE "Datum Ensemble" TO PUBLIC;
+GRANT SELECT ON TABLE "Datum Ensemble Member" TO PUBLIC;
+GRANT SELECT ON TABLE "Datum Realization Method" TO PUBLIC;
+GRANT SELECT ON TABLE "Defining Operation" TO PUBLIC;
+GRANT SELECT ON TABLE "Deprecation" TO PUBLIC;
+GRANT SELECT ON TABLE "Ellipsoid" TO PUBLIC;
+GRANT SELECT ON TABLE "Extent" TO PUBLIC;
+GRANT SELECT ON TABLE "Naming System" TO PUBLIC;
+GRANT SELECT ON TABLE "Prime Meridian" TO PUBLIC;
+GRANT SELECT ON TABLE "Scope" TO PUBLIC;
+GRANT SELECT ON TABLE "Supersession" TO PUBLIC;
+GRANT SELECT ON TABLE "Unit of Measure" TO PUBLIC;
+GRANT SELECT ON TABLE "Usage" TO PUBLIC;
+GRANT SELECT ON TABLE "Version History" TO PUBLIC;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Prepare.sql b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Prepare.sql
index b84c8ad6d4..39e59a71d5 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Prepare.sql
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Prepare.sql
@@ -10,31 +10,31 @@
 -- If enumerated values are not supported by the database, Apache SIS will automatically replace their usage
 -- by the VARCHAR type.
 --
-CREATE TYPE epsg_datum_kind        AS ENUM ('geodetic', 'vertical', 'temporal', 'engineering', 'dynamic geodetic', 'ensemble');
-CREATE TYPE epsg_crs_kind          AS ENUM ('geocentric', 'geographic 2D', 'geographic 3D', 'projected', 'vertical', 'temporal', 'compound', 'engineering', 'derived');
-CREATE TYPE epsg_cs_kind           AS ENUM ('ellipsoidal', 'spherical', 'Cartesian', 'vertical', 'gravity-related', 'time', 'linear', 'polar', 'cylindrical', 'affine', 'ordinal');
-CREATE TYPE epsg_supersession_type AS ENUM ('Supersession');
-CREATE TYPE epsg_table_name        AS ENUM
+CREATE TYPE "Datum Kind"        AS ENUM ('geodetic', 'vertical', 'temporal', 'engineering', 'dynamic geodetic', 'ensemble');
+CREATE TYPE "CRS Kind"          AS ENUM ('geocentric', 'geographic 2D', 'geographic 3D', 'projected', 'vertical', 'temporal', 'compound', 'engineering', 'derived');
+CREATE TYPE "CS Kind"           AS ENUM ('ellipsoidal', 'spherical', 'Cartesian', 'vertical', 'gravity-related', 'time', 'linear', 'polar', 'cylindrical', 'affine', 'ordinal');
+CREATE TYPE "Supersession Type" AS ENUM ('Supersession');
+CREATE TYPE "Table Name"        AS ENUM
    ('Alias',
     'Area',         -- Deprecated (removed in EPSG 10).
-    'AxisName',     -- Deprecated (removed in EPSG 10).
     'Change',
-    'ConventionalRS',
+    'Conventional RS',
     'Coordinate Axis',
     'Coordinate Axis Name',
     'Coordinate_Operation',
     'Coordinate_Operation Method',
     'Coordinate_Operation Parameter',
+    'Coordinate_Operation Parameter Type',   -- Only in online registry.
     'Coordinate_Operation Parameter Usage',
     'Coordinate_Operation Parameter Value',
     'Coordinate_Operation Path',
     'Coordinate Reference System',
     'Coordinate System',
     'Datum',
-    'DatumEnsemble',
-    'DatumEnsembleMember',
-    'DatumRealizationMethod',
-    'DefiningOperation',
+    'Datum Ensemble',
+    'Datum Ensemble Member',
+    'Datum Realization Method',
+    'Defining Operation',
     'Deprecation',
     'Ellipsoid',
     'Extent',
@@ -49,8 +49,8 @@ CREATE TYPE epsg_table_name        AS ENUM
 --
 -- Those casts allow to use enumerated values as if they were VARCHAR elements.
 --
-CREATE CAST (VARCHAR AS epsg_datum_kind)        WITH INOUT AS ASSIGNMENT;
-CREATE CAST (VARCHAR AS epsg_crs_kind)          WITH INOUT AS ASSIGNMENT;
-CREATE CAST (VARCHAR AS epsg_cs_kind)           WITH INOUT AS ASSIGNMENT;
-CREATE CAST (VARCHAR AS epsg_supersession_type) WITH INOUT AS ASSIGNMENT;
-CREATE CAST (VARCHAR AS epsg_table_name)        WITH INOUT AS ASSIGNMENT;
+CREATE CAST (VARCHAR AS "Datum Kind")        WITH INOUT AS ASSIGNMENT;
+CREATE CAST (VARCHAR AS "CRS Kind")          WITH INOUT AS ASSIGNMENT;
+CREATE CAST (VARCHAR AS "CS Kind")           WITH INOUT AS ASSIGNMENT;
+CREATE CAST (VARCHAR AS "Supersession Type") WITH INOUT AS ASSIGNMENT;
+CREATE CAST (VARCHAR AS "Table Name")        WITH INOUT AS ASSIGNMENT;
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/InstallationScriptProvider.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/InstallationScriptProvider.java
index 9e78b8cd3d..096efdad13 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/InstallationScriptProvider.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/InstallationScriptProvider.java
@@ -41,6 +41,7 @@ import org.apache.sis.system.Fallback;
 import org.apache.sis.system.DataDirectory;
 import org.apache.sis.util.privy.CollectionsExt;
 import org.apache.sis.util.privy.Constants;
+import org.apache.sis.pending.jdk.JDK22;
 
 
 /**
@@ -107,7 +108,7 @@ public abstract class InstallationScriptProvider extends InstallationResources {
      *   </tr><tr>
      *     <td>{@code EPSG}</td>
      *     <td class="sep"><code>
-     *       {{@linkplain #PREPARE}, "EPSG_Tables.sql", "EPSG_Data.sql", "EPSG_FKeys.sql", {@linkplain #FINISH}}
+     *       {{@linkplain #PREPARE}, "Tables.sql", "Data.sql", "FKeys.sql", {@linkplain #FINISH}}
      *     </code></td>
      *   </tr>
      * </table>
@@ -273,14 +274,14 @@ public abstract class InstallationScriptProvider extends InstallationResources {
 
 
     /**
-     * The default implementation which uses the scripts in the {@code $SIS_DATA/Databases/ExternalSources}
+     * The default implementation which uses the scripts in the {@code $SIS_DATA/Databases/ExternalSources/EPSG}
      * directory, if present. This class expects the files to have those exact names where {@code *} stands
      * for any characters provided that there is no ambiguity:
      *
      * <ul>
-     *   <li>{@code EPSG_*Tables.sql}</li>
-     *   <li>{@code EPSG_*Data.sql}</li>
-     *   <li>{@code EPSG_*FKeys.sql}</li>
+     *   <li>{@code *Tables*.sql}</li>
+     *   <li>{@code *Data*.sql}</li>
+     *   <li>{@code *FKeys*.sql}</li>
      * </ul>
      *
      * @author  Martin Desruisseaux (Geomatys)
@@ -309,8 +310,8 @@ public abstract class InstallationScriptProvider extends InstallationResources {
             final String[] found = new String[resources.length - (FIRST_FILE + 1)];   // +1 is for omitting `FINISH`.
             @SuppressWarnings("LocalVariableHidesMemberVariable")
             Path directory = DataDirectory.DATABASES.getDirectory();
-            if (directory != null && Files.isDirectory(directory = directory.resolve("ExternalSources"))) {
-                try (DirectoryStream<Path> content = Files.newDirectoryStream(directory, "EPSG_*.sql")) {
+            if (directory != null && Files.isDirectory(directory = JDK22.resolve(directory, "ExternalSources", Constants.EPSG))) {
+                try (DirectoryStream<Path> content = Files.newDirectoryStream(directory, "*.sql")) {
                     for (final Path path : content) {
                         final String name = path.getFileName().toString();
                         for (int i=0; i<found.length; i++) {
@@ -319,6 +320,7 @@ public abstract class InstallationScriptProvider extends InstallationResources {
                                 if (found[i] == null) {
                                     found[i] = name;
                                 } else {
+                                    directory = null;
                                     log(Errors.forLocale(locale).createLogRecord(Level.WARNING,
                                             Errors.Keys.DuplicatedFileReference_1, part));
                                 }
@@ -330,27 +332,18 @@ public abstract class InstallationScriptProvider extends InstallationResources {
                 directory = null;       // Does not exist or is not a directory.
             }
             this.directory = directory;
-            /*
-             * Store the actual file names including the target database. Example:
-             *
-             *   - PostgreSQL_Table_Script.sql
-             *   - PostgreSQL_Data_Script.sql
-             *   - PostgreSQL_FKey_Script.sql
-             *
-             * If a file was not found, use the "EPSG_Foo_Script.sql" name pattern for giving some hints
-             * to users about which file was expected. This filename will appear in the exception message.
-             */
             for (int i=0; i<found.length; i++) {
                 String file = found[i];
                 if (file == null) {
-                    file = "EPSG_" + resources[FIRST_FILE + i] + "_Script.sql";
+                    // File not found. Create a default name which will appear in the exception to be thrown.
+                    file = resources[FIRST_FILE + i] + ".sql";
                 }
                 resources[FIRST_FILE + i] = file;
             }
         }
 
         /**
-         * Returns {@code "EPSG"} if the scripts exist in the {@code ExternalSources} subdirectory,
+         * Returns {@code "EPSG"} if the scripts exist in the {@code ExternalSources/EPSG} subdirectory,
          * or an empty set otherwise.
          *
          * @return {@code "EPSG"} if the SQL scripts for installing the EPSG dataset are available,
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 ca2979436d..ea11bcfd25 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
@@ -55,58 +55,68 @@ import org.apache.sis.referencing.internal.Resources;
  *   <li>{@code SELECT * FROM epsg_CoordinateReferenceSystem} (same as above with {@code "epsg_"} prefix added)</li>
  * </ul>
  *
- * Above possibilities differ in the letter cases, spaces and {@code "epsg_"} prefix (non-exhaustive).
- * Those differences exist between the <abbr>EPSG</abbr> databases distributed in MS-Access format or
- * as <abbr>SQL</abbr> scripts. More differences may exist for handling the differences between version
- * 9 and 10 of the <abbr>EPSG</abbr> database schema.
+ * Some differences are the lower or camel case, the spaces between words and the {@code "epsg_"} prefix.
+ * Those differences exist because the <abbr>EPSG</abbr> database is distributed in two family of formats:
+ * as an MS-Access file and as <abbr>SQL</abbr> scripts, and those two families use different table names.
+ * In addition to format-dependent differences, there is also changes in the database schema between some
+ * versions of the <abbr>EPSG</abbr> dataset.
  *
- * <p>Apache <abbr>SIS</abbr> generally uses the naming convention found in the MS-Access database,
- * because it provides more readable <abbr>SQL</abbr> statements. <abbr>SIS</abbr> also assumes an
- * <abbr>EPSG</abbr> database schema version 10 or latter. If {@link EPSGFactory} is connected to
- * a database which uses a different table naming convention, or to <abbr>EPSG</abbr> version 9,
- * then this {@code SQLTranslator} class will translate the <abbr>SQL</abbr> statements on-the-fly.
- * The following table gives the mapping between the two naming conventions:</p>
+ * <h2>Table naming convention</h2>
+ * For readability reasons,
+ * Apache <abbr>SIS</abbr> generally uses the naming convention found in the MS-Access database,
+ * except for the cases shown in the "Name in Apache <abbr>SIS</abbr>" column of the table below.
+ * The <abbr>SQL</abbr> statements given to the {@link #apply(String)} method use the latter.
+ * The following table gives the mapping between the naming conventions:
  *
  * <table class="sis">
- *   <caption>Table and column names</caption>
- *   <tr><th>Element</th>       <th>Name in MS-Access database</th>                    <th>Name in <abbr>SQL</abbr> scripts</th></tr>
- *   <tr><td>Table</td>         <td>{@code Alias}</td>                                 <td>{@code epsg_alias}</td></tr>
- *   <tr><td>Legacy table</td>  <td>{@code Area}</td>                                  <td>{@code epsg_area}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Change}</td>                                <td>{@code epsg_change}</td></tr>
- *   <tr><td>Table</td>         <td>{@code ConventionalRS}</td>                        <td>{@code epsg_conventionalrs}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate Axis}</td>                       <td>{@code epsg_coordinateaxis}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate Axis Name}</td>                  <td>{@code epsg_coordinateaxisname}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate_Operation}</td>                  <td>{@code epsg_coordoperation}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate_Operation Method}</td>           <td>{@code epsg_coordoperationmethod}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate_Operation Parameter}</td>        <td>{@code epsg_coordoperationparam}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate_Operation Parameter Usage}</td>  <td>{@code epsg_coordoperationparamusage}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate_Operation Parameter Value}</td>  <td>{@code epsg_coordoperationparamvalue}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate_Operation Path}</td>             <td>{@code epsg_coordoperationpath}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate Reference System}</td>           <td>{@code epsg_coordinatereferencesystem}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Coordinate System}</td>                     <td>{@code epsg_coordinatesystem}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Datum}</td>                                 <td>{@code epsg_datum}</td></tr>
- *   <tr><td>Table</td>         <td>{@code DatumEnsemble}</td>                         <td>{@code epsg_datumensemble}</td></tr>
- *   <tr><td>Table</td>         <td>{@code DatumEnsembleMember}</td>                   <td>{@code epsg_datumensemblemember}</td></tr>
- *   <tr><td>Table</td>         <td>{@code DatumRealizationMethod}</td>                <td>{@code epsg_datumrealizationmethod}</td></tr>
- *   <tr><td>Table</td>         <td>{@code DefiningOperation}</td>                     <td>{@code epsg_definingoperation}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Deprecation}</td>                           <td>{@code epsg_deprecation}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Ellipsoid}</td>                             <td>{@code epsg_ellipsoid}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Extent}</td>                                <td>{@code epsg_extent}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Naming System}</td>                         <td>{@code epsg_namingsystem}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Prime Meridian}</td>                        <td>{@code epsg_primemeridian}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Scope}</td>                                 <td>{@code epsg_scope}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Supersession}</td>                          <td>{@code epsg_supersession}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Unit of Measure}</td>                       <td>{@code epsg_unitofmeasure}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Version History}</td>                       <td>{@code epsg_versionhistory}</td></tr>
- *   <tr><td>Table</td>         <td>{@code Usage}</td>                                 <td>{@code epsg_usage}</td></tr>
- *   <tr><td>Column</td>        <td>{@code ORDER}</td>                                 <td>{@code coord_axis_order}</td></tr>
+ *   <caption>Mapping of table names</caption>
+ *   <tr><th>Name in <abbr>SQL</abbr> scripts</th>      <th>Name in MS-Access database</th>                  <th>Name in Apache <abbr>SIS</abbr></th></tr>
+ *   <tr><td>{@code epsg_alias}</td>                    <td>{@code Alias}</td>                               <td>″</td></tr>
+ *   <tr><td>{@code epsg_area}</td>                     <td>{@code Area}</td>                                <td>″</td></tr>
+ *   <tr><td>{@code epsg_change}</td>                   <td>{@code Change}</td>                              <td>″</td></tr>
+ *   <tr><td>{@code epsg_conventionalrs}</td>           <td>{@code ConventionalRS}</td>                      <td>{@code Conventional RS}</td></tr>
+ *   <tr><td>{@code epsg_coordinateaxis}</td>           <td>{@code Coordinate Axis}</td>                     <td>″</td></tr>
+ *   <tr><td>{@code epsg_coordinateaxisname}</td>       <td>{@code Coordinate Axis Name}</td>                <td>″</td></tr>
+ *   <tr><td>{@code epsg_coordoperation}</td>           <td>{@code Coordinate_Operation}</td>                <td>″</td></tr>
+ *   <tr><td>{@code epsg_coordoperationmethod}</td>     <td>{@code Coordinate_Operation Method}</td>         <td>″</td></tr>
+ *   <tr><td>{@code epsg_coordoperationparam}</td>      <td>{@code Coordinate_Operation Parameter}</td>      <td>″</td></tr>
+ *   <tr><td>{@code epsg_coordoperationparamusage}</td> <td>{@code Coordinate_Operation Parameter Usage}</td><td>″</td></tr>
+ *   <tr><td>{@code epsg_coordoperationparamvalue}</td> <td>{@code Coordinate_Operation Parameter Value}</td><td>″</td></tr>
+ *   <tr><td>{@code epsg_coordoperationpath}</td>       <td>{@code Coordinate_Operation Path}</td>           <td>″</td></tr>
+ *   <tr><td>{@code epsg_coordinatereferencesystem}</td><td>{@code Coordinate Reference System}</td>         <td>″</td></tr>
+ *   <tr><td>{@code epsg_coordinatesystem}</td>         <td>{@code Coordinate System}</td>                   <td>″</td></tr>
+ *   <tr><td>{@code epsg_datum}</td>                    <td>{@code Datum}</td>                               <td>″</td></tr>
+ *   <tr><td>{@code epsg_datumensemble}</td>            <td>{@code DatumEnsemble}</td>                       <td>{@code Datum Ensemble}</td></tr>
+ *   <tr><td>{@code epsg_datumensemblemember}</td>      <td>{@code DatumEnsembleMember}</td>                 <td>{@code Datum Ensemble Member}</td></tr>
+ *   <tr><td>{@code epsg_datumrealizationmethod}</td>   <td>{@code DatumRealizationMethod}</td>              <td>{@code Datum Realization Method}</td></tr>
+ *   <tr><td>{@code epsg_definingoperation}</td>        <td>{@code DefiningOperation}</td>                   <td>{@code Defining Operation}</td></tr>
+ *   <tr><td>{@code epsg_deprecation}</td>              <td>{@code Deprecation}</td>                         <td>″</td></tr>
+ *   <tr><td>{@code epsg_ellipsoid}</td>                <td>{@code Ellipsoid}</td>                           <td>″</td></tr>
+ *   <tr><td>{@code epsg_extent}</td>                   <td>{@code Extent}</td>                              <td>″</td></tr>
+ *   <tr><td>{@code epsg_namingsystem}</td>             <td>{@code Naming System}</td>                       <td>″</td></tr>
+ *   <tr><td>{@code epsg_primemeridian}</td>            <td>{@code Prime Meridian}</td>                      <td>″</td></tr>
+ *   <tr><td>{@code epsg_scope}</td>                    <td>{@code Scope}</td>                               <td>″</td></tr>
+ *   <tr><td>{@code epsg_supersession}</td>             <td>{@code Supersession}</td>                        <td>″</td></tr>
+ *   <tr><td>{@code epsg_unitofmeasure}</td>            <td>{@code Unit of Measure}</td>                     <td>″</td></tr>
+ *   <tr><td>{@code epsg_versionhistory}</td>           <td>{@code Version History}</td>                     <td>″</td></tr>
+ *   <tr><td>{@code epsg_usage}</td>                    <td>{@code Usage}</td>                               <td>″</td></tr>
  * </table>
  *
- * Apache <abbr>SIS</abbr> automatically detects which convention is used, regardless the database engine.
+ * Columns have the same name in all formats, with only one exception:
+ * for avoiding confusion with the <abbr>SQL</abbr> keyword of the same name,
+ * the {@code ORDER} column in MS-Access has been renamed {@code coord_axis_order} in <abbr>SQL</abbr> scripts.
+ * Apache <abbr>SIS</abbr> uses the latter.
+ *
+ * <p>Apache <abbr>SIS</abbr> automatically detects which name convention is used, regardless the database engine.
  * For example, it is legal to use the mixed-case variant in a PostgreSQL database
  * even if <abbr>EPSG</abbr> distributes the PostgreSQL scripts in lower-case.
  * The {@code "epsg_"} prefix is redundant with database schema and can be omitted.
- * {@code SQLTranslator} automatically detects which database schema contains the <abbr>EPSG</abbr> tables.
+ * {@code SQLTranslator} automatically detects which database schema contains the <abbr>EPSG</abbr> tables.</p>
+ *
+ * <h2>Versions of <abbr>EPSG</abbr> schema</h2>
+ * Apache <abbr>SIS</abbr> assumes an <abbr>EPSG</abbr> database schema version 10 or latter.
+ * If {@link EPSGFactory} is connected to the <abbr>EPSG</abbr> version 9 database,
+ * then this {@code SQLTranslator} class will modify the <abbr>SQL</abbr> statements on-the-fly.
  *
  * <h2>Thread safety</h2>
  * All {@code SQLTranslator} instances given to the {@link EPSGFactory} constructor
@@ -220,6 +230,8 @@ public class SQLTranslator implements UnaryOperator<String> {
      * Non-null if the {@value #ENUMERATION_COLUMN} column in {@code "Alias"} table uses enumeration instead
      * than character varying. In such case, this field contains the enumeration type. If {@code null}, then
      * then column type is {@code VARCHAR} and the cast can be omitted.
+     *
+     * @see #useEnumerations()
      */
     private String tableNameEnum;
 
@@ -232,7 +244,7 @@ public class SQLTranslator implements UnaryOperator<String> {
     private boolean useBoolean;
 
     /**
-     * Whether the sentinel table "Coordinate_Operation" or a variant has been found.
+     * Whether the sentinel table {@code "Coordinate_Operation"} or a variant has been found.
      * If {@code false}, then {@link EPSGInstaller} needs to be run.
      * Note that the {@linkplain #schema} may be {@code null}.
      *
@@ -389,20 +401,30 @@ check:  for (;;) {
                     break;
                 }
                 case 1: {
+                    table = "Coordinate_Operation";
+                    missingColumns.put("AREA_OF_USE_CODE", "INTEGER");               // Defined in version 9, deprecated in 10+.
+                    missingColumns.put("COORD_OP_SCOPE",   "CHAR(1)");               // Idem.
+                    break;
+                }
+                case 2: {
                     table = "Coordinate Reference System";
                     mayRenameColumns.put("BASE_CRS_CODE", "SOURCE_GEOGCRS_CODE");    // EPSG version 10 → version 9.
+                    missingColumns  .put("AREA_OF_USE_CODE", "INTEGER");             // Defined in version 9, deprecated in 10+.
+                    missingColumns  .put("CRS_SCOPE",        "CHAR(1)");             // Idem.
                     break;
                 }
-                case 2: {
+                case 3: {
                     table = "Datum";
                     mayRenameColumns.put("PUBLICATION_DATE",        "REALIZATION_EPOCH"); // EPSG version 10 → version 9.
                     missingColumns  .put("ANCHOR_EPOCH",            "DOUBLE PRECISION");
                     missingColumns  .put("FRAME_REFERENCE_EPOCH",   "DOUBLE PRECISION");
                     missingColumns  .put("REALIZATION_METHOD_CODE", "INTEGER");
                     missingColumns  .put("CONVENTIONAL_RS_CODE",    "INTEGER");
+                    missingColumns  .put("AREA_OF_USE_CODE",        "INTEGER");           // Defined in version 9, deprecated in 10+.
+                    missingColumns  .put("DATUM_SCOPE",             "CHAR(1)");           // Idem.
                     break;
                 }
-                case 3: {
+                case 4: {
                     table = "Extent";                                                // "Area" in 9, "Extent" in 10.
                     mayRenameColumns.put("EXTENT_CODE",              "AREA_CODE");   // EPSG version 10 → version 9.
                     mayRenameColumns.put("EXTENT_NAME",              "AREA_NAME");
@@ -414,23 +436,23 @@ check:  for (;;) {
                     missingColumns  .put("VERTICAL_EXTENT_MIN",      "DOUBLE PRECISION");
                     missingColumns  .put("VERTICAL_EXTENT_MAX",      "DOUBLE PRECISION");
                     missingColumns  .put("VERTICAL_EXTENT_CRS_CODE", "INTEGER");
-                    missingColumns  .put("TEMPORAL_EXTENT_BEGIN",    "VARCHAR(254)");
-                    missingColumns  .put("TEMPORAL_EXTENT_END",      "VARCHAR(254)");
+                    missingColumns  .put("TEMPORAL_EXTENT_BEGIN",    "CHAR(1)");
+                    missingColumns  .put("TEMPORAL_EXTENT_END",      "CHAR(1)");
                     mayReuse = true;
                     break;
                 }
-                case 4: {
+                case 5: {
                     if (mayRenameColumns.isEmpty()) continue;   // "Extent" table has been found.
                     isArea = true;
                     table = "Area";
                     break;
                 }
-                case 5: {
+                case 6: {
                     isUsage = true;
                     table = "Usage";
                     break;
                 }
-                case 6: {
+                case 7: {
                     if (isUsageTableFound) break check; // The check of `ENUMERATION_COLUMN` is already done.
                     table = "Alias";                    // For checking the type of the `ENUMERATION_COLUMN`.
                     break;
@@ -560,6 +582,15 @@ check:  for (;;) {
         return useBoolean;
     }
 
+    /**
+     * Returns {@code true} if the database uses enumeration values where applicable.
+     * This method use the {@value #ENUMERATION_COLUMN} column as a sentinel value for
+     * detecting whether enumerations are used for the whole <abbr>EPSG</abbr> database.
+     */
+    final boolean useEnumerations() {
+        return tableNameEnum != null;
+    }
+
     /**
      * Converts a mixed-case table name to the convention used in the database.
      * The names of the tables for the two conventions are listed in a table in the Javadoc of this class.
@@ -664,7 +695,7 @@ check:  for (;;) {
             int w = buffer.lastIndexOf(ENUMERATION_COLUMN + "=?");
             if (w >= 0) {
                 w += ENUMERATION_COLUMN.length() + 1;
-                buffer.replace(w, w+1, "CAST(? AS " + tableNameEnum + ')');
+                buffer.replace(w, w+1, "CAST(? AS \"" + tableNameEnum + "\")");
             }
         }
         return buffer.toString();
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 b2afeba4e5..55c0a8653b 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
@@ -69,8 +69,8 @@ final class TableInfo {
      * <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(IdentifiedObject, StringBuilder)} method which
-     * will substitute implementation-neutral objects by the Apache SIS class.
+     * 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.
      */
     static final TableInfo[] EPSG = {
         CRS = new TableInfo(CoordinateReferenceSystem.class,
@@ -187,17 +187,10 @@ final class TableInfo {
     final String nameColumn;
 
     /**
-     * Column type for the type (usually with the {@code "_TYPE"} suffix), or {@code null}.
-     * {@link EPSGDataAccess} and {@link AuthorityCodes} assume that values in this column
-     * are not longer than the maximal length specified in {@value #ENUM_REPLACEMENT}.
+     * Column name for the type (usually with the {@code "_TYPE"} suffix), or {@code null}.
      */
     private final String typeColumn;
 
-    /**
-     * The SQL type to use as a replacement for enumerated values on databases that do not support enumerations.
-     */
-    static final String ENUM_REPLACEMENT = "VARCHAR(80)";
-
     /**
      * Sub-interfaces of {@link #type} to handle, or {@code null} if none.
      */
@@ -257,7 +250,7 @@ final class TableInfo {
 
     /**
      * Returns the class of objects created from the given table. The given table name should be one of
-     * the values enumerated in the {@code epsg_table_name} types of the {@code EPSG_Prepare.sql} file.
+     * the values enumerated in the {@code "Table Name"} types of the {@code EPSG_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"}.
      *
@@ -280,13 +273,14 @@ final class TableInfo {
 
     /**
      * Appends a {@code WHERE} clause together with a condition for searching the specified object.
-     * This method delegates to {@link #where(Class, StringBuilder)} with the type of the given object,
-     * except that some object properties may be inspected for resolving ambiguities.
+     * This method delegates to {@link #where(EPSGDataAccess, Class, StringBuilder)} with the type
+     * of the given object, except that some object properties may be inspected for resolving ambiguities.
      *
-     * @param  object  the object to search in the database.
-     * @param  buffer  where to append the {@code WHERE} clause.
+     * @param  factory  the factory which is writing a <abbr>SQL</abbr> statement.
+     * @param  object   the object to search in the database.
+     * @param  buffer   where to append the {@code WHERE} clause.
      */
-    final void where(final IdentifiedObject object, final StringBuilder buffer) {
+    final void where(final EPSGDataAccess factory, final IdentifiedObject object, final StringBuilder buffer) {
         Class<?> userType = object.getClass();
         if (object instanceof GeodeticCRS) {
             final CoordinateSystem cs = ((GeodeticCRS) object).getCoordinateSystem();
@@ -296,7 +290,7 @@ final class TableInfo {
                 userType = DefaultGeocentricCRS.class;
             }
         }
-        where(userType, buffer);
+        where(factory, userType, buffer);
     }
 
     /**
@@ -310,18 +304,20 @@ final class TableInfo {
      *
      * The caller shall add at least one condition after this method call.
      *
+     * @param  factory   the factory which is writing a <abbr>SQL</abbr> statement.
      * @param  userType  the type specified by the user.
      * @param  buffer    where to append the {@code WHERE} clause.
      * @return the subtype, or {@link #type} if no subtype was found.
      */
-    final Class<?> where(final Class<?> userType, final StringBuilder buffer) {
+    final Class<?> where(final EPSGDataAccess factory, final Class<?> userType, final StringBuilder buffer) {
         buffer.append(" WHERE ");
         if (typeColumn != null) {
             for (int i=0; i<subTypes.length; i++) {
                 final Class<?> candidate = subTypes[i];
                 if (candidate.isAssignableFrom(userType)) {
-                    if (ENUM_REPLACEMENT != null) {
-                        buffer.append("CAST(").append(typeColumn).append(" AS ").append(ENUM_REPLACEMENT).append(')');
+                    if (factory.translator.useEnumerations()) {
+                        buffer.append("CAST(").append(typeColumn).append(" AS ")
+                                .append(EPSGInstaller.ENUM_REPLACEMENT).append(')');
                     } else {
                         buffer.append(typeColumn);
                     }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
index 27d6ecbb75..463ab03524 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
@@ -897,7 +897,7 @@ check:      for (int isTarget=0; ; isTarget++) {        // 0 == source check; 1
                     if (deepEquals(crs1, crs2, mode)) {
                         MathTransform tr1 = this.getMathTransform();
                         MathTransform tr2 = that.getMathTransform();
-                        if (mode == ComparisonMode.ALLOW_VARIANT) try {
+                        if (mode.allowsVariant()) try {
                             final MathTransform before = MathTransforms.linear(
                                     CoordinateSystems.swapAndScaleAxes(crs1.getCoordinateSystem(),
                                                                        crs2.getCoordinateSystem()));
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGInstallerTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGInstallerTest.java
index 6926b7096b..893adf3297 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGInstallerTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGInstallerTest.java
@@ -19,7 +19,6 @@ package org.apache.sis.referencing.factory.sql;
 import java.util.Set;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.regex.Pattern;
 import java.io.IOException;
 import javax.sql.DataSource;
 import java.sql.Connection;
@@ -53,9 +52,10 @@ import org.apache.sis.metadata.sql.TestDatabase;
  * so we want to opportunistically verify the result immediately after database creation
  * by using the {@code EPSGFactory} for creating a few CRS.
  *
- * <p>This test requires that {@code $SIS_DATA/Databases/ExternalSources} directory contains
- * the {@code EPSG_Tables.sql}, {@code EPSG_Data.sql} and {@code EPSG_FKeys.sql} files.
- * Those files can be <a href="https://epsg.org/">downloaded from the source</a> or from
+ * <p>This test requires that the {@code $SIS_DATA/Databases/ExternalSources/EPSG} directory
+ * contains the {@code Tables.sql}, {@code Data.sql} and {@code FKeys.sql} files.
+ * An easy setup is to set the above-cited {@code EPSG} directory as a symbolic link
+ * to the {@code EPSG} directory of the
  * <a href="https://sis.apache.org/source.html#non-free">SIS non-free directory</a>.</p>
  *
  * <p>Every databases created by this test suite exist only in memory.
@@ -71,34 +71,6 @@ public final class EPSGInstallerTest extends TestCaseWithLogs {
         super(Loggers.CRS_FACTORY);
     }
 
-    /**
-     * Tests the {@link EPSGInstaller#REPLACE_STATEMENT} pattern.
-     */
-    @Test
-    public void testReplacePattern() {
-        // Statement as in the EPSG scripts since EPSG version 7.06.
-        assertTrue(Pattern.matches(EPSGInstaller.REPLACE_STATEMENT,
-                "UPDATE epsg_datum\n" +
-                "SET datum_name = replace(datum_name, CHR(182), CHR(10))"));
-
-        // Statement as in the EPSG scripts prior to EPSG version 7.06.
-        assertTrue(Pattern.matches(EPSGInstaller.REPLACE_STATEMENT,
-                "UPDATE epsg_datum\n" +
-                "SET datum_name = replace(datum_name, CHAR(182), CHAR(10))"));
-
-        // Modified statement with mixed-case table name in a schema.
-        assertTrue(Pattern.matches(EPSGInstaller.REPLACE_STATEMENT,
-                "UPDATE epsg.\"Alias\"\n" +
-                "SET object_table_name = replace(object_table_name, CHR(182), CHR(10))"));
-
-        // Like above, but the table name contains a space.
-        assertTrue(Pattern.matches(EPSGInstaller.REPLACE_STATEMENT,
-                "UPDATE epsg.\"Coordinate Axis\"\n" +
-                "SET coord_axis_orientation = replace(coord_axis_orientation, CHR(182), CHR(10))"));
-
-        loggings.assertNoUnexpectedLog();
-    }
-
     /**
      * Returns the SQL scripts needed for testing the database creation,
      * or skip the JUnit test if those scripts are not found.
@@ -109,7 +81,7 @@ public final class EPSGInstallerTest extends TestCaseWithLogs {
     private static InstallationScriptProvider getScripts() throws IOException {
         final var scripts = new InstallationScriptProvider.Default(null);
         assumeTrue(scripts.getAuthorities().contains(Constants.EPSG),
-                "EPSG scripts not found in the \"Databases/ExternalSources\" directory.");
+                "Scripts not found in the \"Databases/ExternalSources/EPSG\" directory.");
         return scripts;
     }
 
@@ -257,7 +229,7 @@ public final class EPSGInstallerTest extends TestCaseWithLogs {
     }
 
     /**
-     * Verifies some parameter values in the database. We perform this check on a parameter which are known
+     * Verifies some parameter values in the database. We perform this check on parameters which are known
      * to have small values, in order to make sure that the values have not been truncated to zero.
      */
     private static void verifyParameterValues(final DataSource ds) throws SQLException {
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK22.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK22.java
index adc7887cf7..e7f2c31c5f 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK22.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK22.java
@@ -17,6 +17,7 @@
 package org.apache.sis.pending.jdk;
 
 import java.io.Console;
+import java.nio.file.Path;
 
 
 /**
@@ -31,6 +32,16 @@ public final class JDK22 {
     private JDK22() {
     }
 
+    /**
+     * Placeholder for {@code Path.resolve(String, String...)}.
+     */
+    public static Path resolve(Path path, String... more) {
+        for (String p : more) {
+            path = path.resolve(p);
+        }
+        return path;
+    }
+
     /**
      * Placeholder for {@code Console.isTerminal()}.
      */
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ComparisonMode.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ComparisonMode.java
index 49860713e3..ed337dad42 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ComparisonMode.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/ComparisonMode.java
@@ -41,7 +41,7 @@ package org.apache.sis.util;
  * {@link #IGNORE_METADATA} level but not necessarily at the {@link #STRICT} level.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.5
  *
  * @see LenientComparable#equals(Object, ComparisonMode)
  * @see Utilities#deepEquals(Object, Object, ComparisonMode)
@@ -115,13 +115,15 @@ public enum ComparisonMode {
      * A <q>Mercator (variant B)</q> projection with a <i>standard parallel</i> value of 60° produces the
      * same results as a <q>Mercator (variant A)</q> projection with a <i>scale factor</i> value of 0.5.
      *
+     * @see #isIgnoringMetadata()
      * @see org.apache.sis.util.Utilities#equalsIgnoreMetadata(Object, Object)
      */
     IGNORE_METADATA,
 
     /**
-     * Only the attributes relevant to the object functionality are compared, with some tolerance
-     * threshold on numerical values.
+     * Only the attributes relevant to the object functionality are compared, with some tolerance* threshold on numerical values.
+     * The threshold is implementation dependent, but the current <abbr>SIS</abbr> implementation generally aims for a precision
+     * of 1 centimeter in the linear and angular parameter values for a planet of the size of Earth.
      *
      * <h4>Application to coordinate operations</h4>
      * If two {@link org.opengis.referencing.operation.MathTransform} objects are considered equal according this mode,
@@ -131,6 +133,8 @@ public enum ComparisonMode {
      * How small is “small” is implementation dependent — the threshold cannot be specified in the current
      * implementation, because of the non-linear nature of map projections.
      *
+     * @see #isApproximate()
+     *
      * @since 1.0
      */
     APPROXIMATE,
@@ -138,17 +142,30 @@ public enum ComparisonMode {
     /**
      * Most but not all attributes relevant to the object functionality are compared.
      * This comparison mode is equivalent to {@link #APPROXIMATE}, except that it
-     * ignores some attributes that may differ between objects not equal but related.
+     * ignores some aspects that may differ between objects not equal but related.
+     * Below is a list of examples where the objects being compared would be
+     * considered different according the other modes such as {@link #APPROXIMATE},
+     * but are considered equivalent according this {@code ALLOW_VARIANT} mode.
      *
-     * <p>The main purpose of this method is to verify if two Coordinate Reference Systems (CRS)
-     * are approximately equal ignoring axis order.</p>
+     * <ul class="verbose">
+     *   <li>Two Coordinate Reference Systems (<abbr>CRS</abbr>) with the same axes but in different order.
+     *       <b>Example:</b> two geographic <abbr>CRS</abbr>s with the same attributes but with
+     *       (<var>latitude</var>, <var>longitude</var>) axes in one case and
+     *       (<var>longitude</var>, <var>latitude</var>) axes in the other case.</li>
+     *   <li>Two Coordinate Reference Systems where one is associated to a datum and the other to a datum ensemble,
+     *       but the datum and the ensemble share a common authority code or have an
+     *       {@link org.apache.sis.referencing.IdentifiedObjects#isHeuristicMatchForName equivalent name}.
+     *       <b>Example:</b> {@code EPSG:9:4326} and {@code EPSG:10:4326}, which are both the same (at least conceptually)
+     *       geographic <abbr>CRS</abbr> associated to the authority code 4326 in the <abbr>EPSG</abbr> geodetic dataset,
+     *       but as defined in version 9 and 10 respectively of the <abbr>EPSG</abbr> database.
+     *       They are the <abbr>CRS</abbr> definitions before and after the introduction of datum ensemble in the schema.</li>
+     *   <li>Two Reference Frames which share a common authority code or have an
+     *       {@link org.apache.sis.referencing.IdentifiedObjects#isHeuristicMatchForName equivalent name},
+     *       despite one frame being static and the other frame being dynamic.
+     *       <b>Example:</b> the "WGS 1972" reference frame as defined in versions 9 and 10 of <abbr>EPSG</abbr> database.</li>
+     * </ul>
      *
-     * <h4>Example</h4>
-     * Consider two geographic coordinate reference systems with the same attributes except axis order,
-     * where one CRS uses (<var>latitude</var>, <var>longitude</var>) axes
-     * and the other CRS uses (<var>longitude</var>, <var>latitude</var>) axes.
-     * All comparison modes (even {@code APPROXIMATE}) will consider those two CRS as different,
-     * except this {@code ALLOW_VARIANT} mode which will consider one CRS to be a variant of the other.
+     * @see #allowsVariant()
      *
      * @since 0.7
      */
@@ -172,8 +189,8 @@ public enum ComparisonMode {
 
     /**
      * Returns {@code true} if this comparison ignores metadata.
-     * This method currently returns {@code true} for {@code IGNORE_METADATA}, {@code APPROXIMATE}
-     * or {@code DEBUG} only, but this list may be extended in future SIS versions.
+     * This method currently returns {@code true} for {@link #IGNORE_METADATA}, {@link #APPROXIMATE},
+     * {@link #ALLOW_VARIANT} or {@link #DEBUG}, but this list may be extended in future <abbr>SIS</abbr> versions.
      *
      * @return whether this comparison ignore metadata.
      *
@@ -185,8 +202,8 @@ public enum ComparisonMode {
 
     /**
      * Returns {@code true} if this comparison uses a tolerance threshold.
-     * This method currently returns {@code true} for {@code APPROXIMATE} or {@code DEBUG} only,
-     * but this list may be extended in future SIS versions.
+     * This method currently returns {@code true} for {@link #APPROXIMATE}, {@link #ALLOW_VARIANT}
+     * or {@link #DEBUG}, but this list may be extended in future <abbr>SIS</abbr> versions.
      *
      * @return whether this comparison uses a tolerance threshold.
      *
@@ -196,6 +213,19 @@ public enum ComparisonMode {
         return ordinal() >= APPROXIMATE.ordinal();
     }
 
+    /**
+     * Returns {@code true} if this comparison allow variants.
+     * This method currently returns {@code true} for {@link #ALLOW_VARIANT}
+     * or {@link #DEBUG}, but this list may be extended in future <abbr>SIS</abbr> versions.
+     *
+     * @return whether this comparison allows variants.
+     *
+     * @since 1.5
+     */
+    public boolean allowsVariant() {
+        return ordinal() >= ALLOW_VARIANT.ordinal();
+    }
+
     /**
      * If the two given objects are equal according one of the modes enumerated in this class,
      * then returns that mode. Otherwise returns {@code null}.
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/StringBuilders.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/StringBuilders.java
index d9d71431b1..5d38dfecc8 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/StringBuilders.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/StringBuilders.java
@@ -34,7 +34,7 @@ import org.apache.sis.util.resources.Errors;
  * the <i>Basic Multilingual Plane</i> (BMP).
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.5
  *
  * @see CharSequences
  *
@@ -56,6 +56,27 @@ public final class StringBuilders extends Static {
     private StringBuilders() {
     }
 
+    /**
+     * Removes leading and trailing whitespace characters in a subregion of the specified buffer.
+     * Space characters are identified by the {@link Character#isWhitespace(int)} method.
+     *
+     * @param  buffer  the buffer where to remove leading and trailing white spaces.
+     * @param  lower   index of the first character of the subregion where to remove leading spaces.
+     * @param  upper   index after the last character of the subregion where to remove trailing spaces.
+     * @return number of characters removed.
+     *
+     * @see CharSequences#trimWhitespaces(CharSequence, int, int)
+     *
+     * @since 1.5
+     */
+    public static int trimWhitespaces(final StringBuilder buffer, final int lower, final int upper) {
+        ArgumentChecks.ensureNonNull("buffer", buffer);
+        int i = CharSequences.skipTrailingWhitespaces(buffer, lower, upper);
+        final int length = buffer.length();
+        buffer.delete(i, upper).delete(lower, CharSequences.skipLeadingWhitespaces(buffer, lower, i));
+        return length - buffer.length();
+    }
+
     /**
      * Replaces every occurrences of the given character in the given buffer.
      *
diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/StringBuildersTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/StringBuildersTest.java
index 604019c26d..dd6ca8eff9 100644
--- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/StringBuildersTest.java
+++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/StringBuildersTest.java
@@ -37,12 +37,25 @@ public final class StringBuildersTest extends TestCase {
     public StringBuildersTest() {
     }
 
+    /**
+     * Tests the {@link StringBuilders#trimWhitespaces(StringBuilder, int, int)} method.
+     */
+    @Test
+    public void testTrimWhitespaces() {
+        final var buffer = new StringBuilder("   Foo bar ");
+        int expected = 4;
+        do {
+            assertEquals(expected, StringBuilders.trimWhitespaces(buffer, 0, buffer.length()));
+            assertEquals("Foo bar", buffer.toString());
+        } while ((expected ^= 4) == 0);
+    }
+
     /**
      * Tests the {@link StringBuilders#replace(StringBuilder, String, String)} method.
      */
     @Test
     public void testReplace() {
-        final StringBuilder buffer = new StringBuilder("One two three two one");
+        final var buffer = new StringBuilder("One two three two one");
         replace(buffer, "two", "zero");
         assertEquals("One zero three zero one", buffer.toString());
         replace(buffer, "zero", "ten");
@@ -54,7 +67,7 @@ public final class StringBuildersTest extends TestCase {
      */
     @Test
     public void testReplaceChar() {
-        final StringBuilder buffer = new StringBuilder("ABCDEFBCD");
+        final var buffer = new StringBuilder("ABCDEFBCD");
         replace(buffer, 'B', '*');
         assertEquals("A*CDEF*CD", buffer.toString());
     }
@@ -64,7 +77,7 @@ public final class StringBuildersTest extends TestCase {
      */
     @Test
     public void testReplaceChars() {
-        final StringBuilder buffer = new StringBuilder("ABCD1234EFGH");
+        final var buffer = new StringBuilder("ABCD1234EFGH");
         replace(buffer, 4, 8, new char[] {'5','6','7','8'});
         assertEquals("ABCD5678EFGH", buffer.toString());
         replace(buffer, 4, 6, new char[] {'1','2','3','4'});
@@ -78,7 +91,7 @@ public final class StringBuildersTest extends TestCase {
      */
     @Test
     public void testRemove() {
-        final StringBuilder buffer = new StringBuilder("EPSG.6.7");
+        final var buffer = new StringBuilder("EPSG.6.7");
         remove(buffer, ".");
         assertEquals("EPSG67", buffer.toString());
     }
@@ -88,7 +101,7 @@ public final class StringBuildersTest extends TestCase {
      */
     @Test
     public void testRepeat() {
-        final StringBuilder buffer = new StringBuilder("AB12");
+        final var buffer = new StringBuilder("AB12");
         repeat(buffer, 2, 'C', 0);
         assertEquals("AB12", buffer.toString());
         repeat(buffer, 2, 'C', 1);
@@ -106,7 +119,7 @@ public final class StringBuildersTest extends TestCase {
      */
     @Test
     public void testTrimFractionalPart() {
-        final StringBuilder buffer = new StringBuilder("4.10");
+        final var buffer = new StringBuilder("4.10");
         trimFractionalPart(buffer);
         assertEquals("4.10", buffer.toString());
         buffer.setCharAt(2, '0');                                   // Replace the '1' by '0'.
@@ -121,7 +134,7 @@ public final class StringBuildersTest extends TestCase {
      */
     @Test
     public void testToASCII() {
-        final StringBuilder s = new StringBuilder(
+        final var s = new StringBuilder(
                 "mètres" + Characters.PARAGRAPH_SEPARATOR +
                 " ‘single’, “double”, \"ascii' 30°20′10″.");
         toASCII(s);
diff --git a/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java b/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java
index dc3f7b2eaa..af3950e91b 100644
--- a/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java
+++ b/incubator/src/org.apache.sis.storage.geopackage/main/org/apache/sis/storage/geopackage/GpkgStore.java
@@ -226,7 +226,7 @@ public class GpkgStore extends SQLStore implements WritableAggregate {
         assert Thread.holdsLock(this) && userContents.isEmpty();
         if (create) try {
             cnx.setAutoCommit(false);
-            try (ScriptRunner runner = new ScriptRunner(cnx, 100)) {
+            try (ScriptRunner runner = new ScriptRunner(cnx, null, 100)) {
                 runner.run("PRAGMA main.application_id = " + GpkgStoreProvider.APPLICATION_ID + ';'
                          + "PRAGMA main.user_version = " + GpkgStoreProvider.VERSION + ';');
                 runner.run("Core.sql", GpkgStore.class.getResourceAsStream("Core.sql"));
diff --git a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/Clear.sql b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/Clear.sql
new file mode 100644
index 0000000000..00e3f0e276
--- /dev/null
+++ b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/Clear.sql
@@ -0,0 +1,43 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+
+
+--
+-- Delete all EPSG tables. Useful for repeated tests of data insertions.
+--
+
+DROP TABLE "Alias" CASCADE;
+DROP TABLE "Change" CASCADE;
+DROP TABLE "Conventional RS" CASCADE;
+DROP TABLE "Coordinate Axis" CASCADE;
+DROP TABLE "Coordinate Axis Name" CASCADE;
+DROP TABLE "Coordinate Reference System" CASCADE;
+DROP TABLE "Coordinate System" CASCADE;
+DROP TABLE "Coordinate_Operation" CASCADE;
+DROP TABLE "Coordinate_Operation Method" CASCADE;
+DROP TABLE "Coordinate_Operation Parameter" CASCADE;
+DROP TABLE "Coordinate_Operation Parameter Usage" CASCADE;
+DROP TABLE "Coordinate_Operation Parameter Value" CASCADE;
+DROP TABLE "Coordinate_Operation Path" CASCADE;
+DROP TABLE "Datum" CASCADE;
+DROP TABLE "Datum Ensemble" CASCADE;
+DROP TABLE "Datum Ensemble Member" CASCADE;
+DROP TABLE "Datum Realization Method" CASCADE;
+DROP TABLE "Defining Operation" CASCADE;
+DROP TABLE "Deprecation" CASCADE;
+DROP TABLE "Ellipsoid" CASCADE;
+DROP TABLE "Extent" CASCADE;
+DROP TABLE "Naming System" CASCADE;
+DROP TABLE "Prime Meridian" CASCADE;
+DROP TABLE "Scope" CASCADE;
+DROP TABLE "Supersession" CASCADE;
+DROP TABLE "Unit of Measure" CASCADE;
+DROP TABLE "Usage" CASCADE;
+DROP TABLE "Version History" CASCADE;
+DROP TYPE  "Datum Kind" CASCADE;
+DROP TYPE  "CRS Kind" CASCADE;
+DROP TYPE  "CS Kind" CASCADE;
+DROP TYPE  "Supersession Type" CASCADE;
+DROP TYPE  "Table Name" CASCADE;
diff --git a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DataScriptFormatter.java b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DataScriptFormatter.java
index cd8dd320f4..8795ce9762 100644
--- a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DataScriptFormatter.java
+++ b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DataScriptFormatter.java
@@ -17,72 +17,34 @@
 package org.apache.sis.referencing.factory.sql.epsg;
 
 import java.util.Map;
-import java.util.HashMap;
-import java.util.function.UnaryOperator;
-import java.io.LineNumberReader;
-import java.io.InputStreamReader;
-import java.io.Writer;
+import java.util.Set;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Files;
-import java.nio.charset.StandardCharsets;
 import java.sql.Connection;
 import java.sql.SQLException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.StringBuilders;
 import org.apache.sis.util.privy.URLs;
 import org.apache.sis.metadata.sql.privy.SQLBuilder;
 import org.apache.sis.metadata.sql.privy.ScriptRunner;
 
-// Test dependencies
-import org.apache.sis.metadata.sql.TestDatabase;
-
 
 /**
- * Rewrites the {@code INSERT TO ...} statements in a SQL script in a more compact form.
- * This class is used only for updating the <abbr>SQL</abbr> scripts used by Apache SIS
+ * Rewrites the {@code INSERT TO ...} statements of a <abbr>SQL</abbr> script in a more compact form.
+ * This class is used only for updating the <abbr>SQL</abbr> scripts used by Apache <abbr>SIS</abbr>
  * for the <abbr>EPSG</abbr> dataset when a newer release of that dataset is available.
  * The steps to follow are documented in the {@code README.md} file.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  */
-public final class DataScriptFormatter extends ScriptRunner {
-    /**
-     * Compacts the {@code *Data_Script.sql} file provided by <abbr>EPSG</abbr>.
-     * This method expects two arguments:
-     *
-     * <ol>
-     *   <li>The file of the <abbr>SQL</abbr> script to convert, which must exist.</li>
-     *   <li>The file where to write the compacted script, which will be overwritten without warning if it exists.</li>
-     * </ol>
-     *
-     * The values of those arguments are typically:
-     *
-     * <ol>
-     *   <li>{@code $EPSG_SCRIPTS/PostgreSQL_Data_Script.sql}</li>
-     *   <li>{@code sis-epsg/src/main/resources/org/apache/sis/referencing/factory/sql/epsg/Data.sql}</li>
-     * </ol>
-     *
-     * @param  arguments  the source files and the destination file.
-     * @throws Exception if an error occurred while reading of writing the file.
-     */
-    @SuppressWarnings("UseOfSystemOutOrSystemErr")
-    public static void main(String[] arguments) throws Exception {
-        if (arguments.length != 2) {
-            System.err.println("Expected two arguments: source SQL file and target SQL file.");
-            return;
-        }
-        try (TestDatabase db = TestDatabase.create("dummy");
-             Connection c = db.source.getConnection())
-        {
-            final var formatter = new DataScriptFormatter(c);
-            formatter.run(Path.of(arguments[0]), Path.of(arguments[1]));
-        }
-    }
-
+final class DataScriptFormatter extends ScriptRunner {
     /**
      * The {@value} keywords.
      */
@@ -94,45 +56,68 @@ public final class DataScriptFormatter extends ScriptRunner {
     private static final String VALUES = "VALUES";
 
     /**
-     * The output stream, or {@code null} if closed or not yet created.
+     * Mapping from the table names in <abbr>SQL</abbr> scripts to the names used in {@code EPSGDataAccess}.
+     * Those names are derived from the names used in the dataset distributed in MS-Access format.
+     * We use those original names because they are easier to read than the names in SQL scripts.
      */
-    private Writer out;
+    private final Map<String,String> toOriginalTableNames;
 
     /**
-     * The {@code INSERT INTO "Table"} statement currently executing.
-     * Used in order to detect when the script start inserting values in a different table.
+     * Names of the columns to search for computing {@link TableValues#booleanColumnIndices}.
      */
-    private String insertStatement;
+    private final Set<String> booleanColumns;
 
     /**
-     * Indices (in reversal order) of columns to change from type SMALLINT to type BOOLEAN.
-     * Index 0 is the last column, index 1 is the column before the last, <i>etc</i>.
-     * We use the reverse order because most Boolean columns in the EPSG dataset are last.
+     * Names of the columns to search for computing {@link TableValues#doubleColumnIndices}.
+     * We do not reformat the east/west/north/south bounds, the {@code greenwich_longitude},
+     * the {@code semi_major_axis}, the {@code semi_minor_axis} and the {@code inv_flattening}
+     * columns because their values are close to integers,
+     */
+    private final Set<String> doubleColumns;
+
+    /**
+     * Names of the columns to exclude. Some are columns deprecated since <abbr>EPSG</abbr> version 10+,
+     * but still present for compatibility reasons with only null values in every rows. Other columns are
+     * <abbr>EPSG</abbr> metadata not used by <abbr>SIS</abbr> (e.g. change records).
+     *
+     * <p>The following columns are replaced by the {@code "Usage"} intersection table,
+     * but <abbr>EPSG</abbr> keeps the old columns filled with null values.</p>
+     * <ul>
+     *   <li>{@code area_of_use_code}: appeared in {@code "Coordinate Reference System"}, {@code "Coordinate_Operation"} and {@code "Datum"}.</li>
+     *   <li>{@code crs_scope}: appeared in {@code "Coordinate Reference System"}.</li>
+     *   <li>{@code coord_op_scope}: appeared in {@code "Coordinate_Operation"}.</li>
+     *   <li>{@code datum_scope}: appeared in {@code "Datum"}.</li>
+     * </ul>
+     *
+     * <p>The following columns are deprecated for other reasons:</p>
+     * <ul>
+     *   <li>{@code realization_epoch}: is replaced by {@code publication_date}.</li>
+     * </ul>
+     *
+     * <p>The following columns contains <abbr>EPSG</abbr> metadata not used by Apache<abbr>SIS</abbr>.
+     * They are lineage and could be included in a future version if we can map them to ISO 19115.</p>
+     * <ul>
+     *   <li>{@code information_source}, {@code data_source} and {@code revision_date}: appeared in almost all tables.</li>
+     *   <li>{@code change_id}: appeared in almost all tables. Required by the removal of the {@code "Change"} table.</li>
+     * </ul>
      */
-    private int[] booleanColumnIndices;
+    private final Set<String> excludedColumns;
 
     /**
-     * The {@link #booleanColumnIndices} value for each table.
+     * The values for each table. The array length is the maximal number of tables that we expect in an EPSG schema.
+     * An {@link ArrayIndexOutOfBoundsException} when reading this array would mean that the schema has changed.
      */
-    private final Map<String,int[]> booleanColumnIndicesForTables;
+    private final TableValues[] valuesPerTable;
 
     /**
-     * Indices (in reversal order) of columns to reformat as a double value.
-     * The EPSG database contains values like {@code -0.000000000000000000000003689471323},
-     * while is either not parsed by Derby because too long (Derby 10.14 documents a limit
-     * of 30 characters for floating-point constants) or parsed as 0. To workaround such
-     * limitation, we reformat above value as {@code -3.689471323E-24}.
+     * Number of valid values in {@link #valuesPerTable}.
      */
-    private int[] doubleColumnIndices;
+    private int tableCount;
 
     /**
-     * The {@link #doubleColumnIndices} value of each table for which we want to reformat the value.
-     * We do not reformat the {@code change_id} columns since they are more like character strings.
-     * We do not reformat east/west/north/south bounds or {@code greenwich_longitude} since their
-     * values are close to integers, or {@code semi_major_axis}, {@code semi_minor_axis} and
-     * {@code inv_flattening} for similar reasons.
+     * Statements other than {@code INSERT INTO}.
      */
-    private final Map<String,int[]> doubleColumnIndicesForTables;
+    private final List<String> otherStatements;
 
     /**
      * Creates a new instance.
@@ -140,37 +125,49 @@ public final class DataScriptFormatter extends ScriptRunner {
      * @param  c  a dummy connection. Will be used for fetching metadata.
      * @throws SQLException if an error occurred while fetching metadata.
      */
-    private DataScriptFormatter(final Connection c) throws SQLException {
-        super(c, Integer.MAX_VALUE);
-        final int[]    lastColumn  = new int[] {0  };
-        final int[] twoLastColumns = new int[] {0,1};
-        final var m = new HashMap<String,int[]>();
-        m.put("epsg_area",                         lastColumn );
-        m.put("epsg_coordinateaxisname",           lastColumn );
-        m.put("epsg_coordinatereferencesystem", twoLastColumns);
-        m.put("epsg_coordinatesystem",             lastColumn );
-        m.put("epsg_coordoperation",            twoLastColumns);
-        m.put("epsg_coordoperationmethod",     new int[] {0,8});
-        m.put("epsg_coordoperationparam",          lastColumn );
-        m.put("epsg_coordoperationparamusage",     lastColumn );
-        m.put("epsg_datum",                        lastColumn );
-        m.put("epsg_ellipsoid",                new int[] {0,6});
-        m.put("epsg_namingsystem",                 lastColumn );
-        m.put("epsg_primemeridian",                lastColumn );
-        m.put("epsg_unitofmeasure",                lastColumn );
-        booleanColumnIndicesForTables = m;
-        doubleColumnIndicesForTables = Map.of("epsg_coordoperationparamvalue", new int[] {2});
-    }
+    DataScriptFormatter(final Connection c) throws SQLException {
+        super(c, null, Integer.MAX_VALUE);
+        booleanColumns  = Set.of("deprecated", "show_crs", "show_operation", "reverse_op", "param_sign_reversal", "ellipsoid_shape");
+        doubleColumns   = Set.of("parameter_value");
+        excludedColumns = Set.of("area_of_use_code", "crs_scope", "coord_op_scope", "datum_scope",    // Replaced by "Usage" table.
+                                 "information_source", "data_source", "revision_date", "change_id",   // Unused EPSG metadata.
+                                 "realization_epoch");
 
-    /**
-     * Returns {@code true} if the given line should be omitted from the script.
-     *
-     * @param  line  the line, without trailing {@code ';'}.
-     * @return {@code true} if the line should be omitted.
-     */
-    private static boolean omit(final String line) {
-        // We omit the following line because we changed the type from VARCHAR to DATE.
-        return line.startsWith("UPDATE epsg_datum SET realization_epoch = replace(realization_epoch, CHR(182), CHR(10))");
+        toOriginalTableNames = Map.ofEntries(
+                Map.entry("epsg_alias",                      "Alias"),
+                Map.entry("epsg_area",                       "Area"),      // Deprecated (removed in EPSG 10)
+                Map.entry("epsg_change",                     "Change"),
+                Map.entry("epsg_conventionalrs",             "Conventional RS"),
+                Map.entry("epsg_coordinateaxis",             "Coordinate Axis"),
+                Map.entry("epsg_coordinateaxisname",         "Coordinate Axis Name"),
+                Map.entry("epsg_coordoperation",             "Coordinate_Operation"),
+                Map.entry("epsg_coordoperationmethod",       "Coordinate_Operation Method"),
+                Map.entry("epsg_coordoperationparam",        "Coordinate_Operation Parameter"),
+                Map.entry("epsg_coordoperationparamtype",    "Coordinate_Operation Parameter Type"),    // Only in the online registry.
+                Map.entry("epsg_coordoperationparamusage",   "Coordinate_Operation Parameter Usage"),
+                Map.entry("epsg_coordoperationparamvalue",   "Coordinate_Operation Parameter Value"),
+                Map.entry("epsg_coordoperationpath",         "Coordinate_Operation Path"),
+                Map.entry("epsg_coordinatereferencesystem",  "Coordinate Reference System"),
+                Map.entry("epsg_coordinatesystem",           "Coordinate System"),
+                Map.entry("epsg_datum",                      "Datum"),
+                Map.entry("epsg_datumensemble",              "Datum Ensemble"),
+                Map.entry("epsg_datumensemblemember",        "Datum Ensemble Member"),
+                Map.entry("epsg_datumrealizationmethod",     "Datum Realization Method"),
+                Map.entry("epsg_definingoperation",          "Defining Operation"),
+                Map.entry("epsg_deprecation",                "Deprecation"),
+                Map.entry("epsg_ellipsoid",                  "Ellipsoid"),
+                Map.entry("epsg_extent",                     "Extent"),
+                Map.entry("epsg_namingsystem",               "Naming System"),
+                Map.entry("epsg_primemeridian",              "Prime Meridian"),
+                Map.entry("epsg_scope",                      "Scope"),
+                Map.entry("epsg_supersession",               "Supersession"),
+                Map.entry("epsg_unitofmeasure",              "Unit of Measure"),
+                Map.entry("epsg_usage",                      "Usage"),
+                Map.entry("epsg_versionhistory",             "Version History"));
+
+        toOriginalTableNames.forEach((oldTable, newTable) -> addReplacement(oldTable, '"' + newTable + '"'));
+        valuesPerTable  = new TableValues[toOriginalTableNames.size()];
+        otherStatements = new ArrayList<>();
     }
 
     /**
@@ -181,60 +178,85 @@ public final class DataScriptFormatter extends ScriptRunner {
      * @throws IOException  if an I/O operation failed.
      * @throws SQLException should never happen.
      */
-    private void run(final Path inputFile, final Path outputFile) throws SQLException, IOException {
-        if (inputFile.equals(outputFile)) {
+    final void run(final Path inputFile, final Path outputFile) throws SQLException, IOException {
+        if (Files.isSameFile(inputFile, outputFile)) {
             throw new IllegalArgumentException("Input and output files are the same.");
         }
-        out = Files.newBufferedWriter(outputFile);
-        try (LineNumberReader in = new LineNumberReader(new InputStreamReader(Files.newInputStream(inputFile), StandardCharsets.UTF_8))) {
+        try (BufferedReader in = Files.newBufferedReader(inputFile)) {
+            run(inputFile.getFileName().toString(), in);
+        }
+        try (BufferedWriter out = Files.newBufferedWriter(outputFile)) {
             out.write("---\n" +
                       "---    Copyright International Association of Oil and Gas Producers (IOGP)\n" +
                       "---    See  " + URLs.EPSG_LICENSE + "  (a copy is in ./LICENSE.txt).\n" +
                       "---\n" +
-                      "---    This file has been reformatted (without any change in the data) for the needs of Apache SIS project.\n" +
+                      "---    This file has been reformatted for the needs of Apache SIS project.\n" +
                       "---    See org.apache.sis.referencing.factory.sql.epsg.DataScriptFormatter.\n" +
                       "---\n" +
                       "\n");
-            run(inputFile.getFileName().toString(), in);
-        } finally {
-            out.close();
-            out = null;
+
+            // Sort in order of table name for consistency with previous versions (smaller SVN diff).
+            Arrays.sort(valuesPerTable, (t1, t2) -> t1.insertStatement.compareTo(t2.insertStatement));
+            for (int i=0; i<tableCount; i++) {
+                valuesPerTable[i].write(out);
+            }
+            for (String other : otherStatements) {
+                out.write(other);
+                out.write(";\n");       // Really want Unix EOL, not the platform-specific one.
+            }
         }
     }
 
     /**
+     * Replaces the content of a text such as {@code 'some text'}.
+     * If the text content is a table name, the old table name is replaced by the new name.
+     *
+     * <h4>Corrections</h4>
      * EPSG scripts version 8.9 seems to have 2 errors where the {@code OBJECT_TABLE_NAME} column contains
      * {@code "AxisName"} instead of {@code "Coordinate Axis Name"}. Furthermore, the version number noted
      * in the history table is a copy-and-paste error.
+     *
+     * @param  sql    the whole SQL statement.
+     * @param  lower  index of the opening quote character ({@code '}) of the text in {@code sql}.
+     * @param  upper  index after the closing quote character ({@code '}) of the text in {@code sql}.
      */
     @Override
     @Workaround(library="EPSG", version="8.9")
     protected void editText(final StringBuilder sql, int lower, int upper) {
+        final String text = sql.substring(++lower, --upper);
+        final String tableName = toOriginalTableNames.get(text);
+        if (tableName != null) {
+            sql.replace(lower, upper, tableName);
+            return;
+        }
         final String table;         // Name of the table where to replace a value.
         final String before;        // String that must exist before the value to replace, or null if none.
         final String oldValue;      // The old value to replace.
         final String newValue;      // The new value.
         switch (upper - lower) {    // Optimization for reducing the number of comparisons.
-            default: return;
-            case 10: {
-                table    = "epsg_deprecation";
+            default: {
+                StringBuilders.trimWhitespaces(sql, lower, upper);
+                return;
+            }
+            case 8: {
+                table    = "Deprecation";
                 before   = null;
                 oldValue = "AxisName";
                 newValue = "Coordinate Axis Name";
                 break;
             }
-            case 38: {
-                table    = "epsg_versionhistory";
+            case 36: {
+                table    = "Version History";
                 before   = "'8.9'";
                 oldValue = "Version 8.8 full release of Dataset.";
                 newValue = "Version 8.9 full release of Dataset.";
                 break;
             }
         }
-        if (CharSequences.regionMatches(sql, ++lower, oldValue)) {
+        if (oldValue.equalsIgnoreCase(text)) {
             final int s = CharSequences.skipLeadingWhitespaces(sql, 0, lower);
-            if (CharSequences.regionMatches(sql, s, SQLBuilder.INSERT + table + " VALUES")) {
-                if (--upper - lower != oldValue.length()) {
+            if (CharSequences.regionMatches(sql, s, SQLBuilder.INSERT + '"' + table + '"')) {
+                if (upper - lower != oldValue.length()) {
                     throw new AssertionError("Unexpected length");
                 }
                 if (before != null) {
@@ -244,12 +266,13 @@ public final class DataScriptFormatter extends ScriptRunner {
                 sql.replace(lower, upper, newValue);
             }
         }
+        StringBuilders.trimWhitespaces(sql, lower, upper);
     }
 
     /**
      * Makes sure that {@link #execute(StringBuilder)} is invoked for every line. Whether the SQL statement
      * is supported or not is irrelevant for this method since we do not know yet what will be the database
-     * engine; we just copy the SQL statements in a file without executing them.
+     * engine. We just copy the <abbr>SQL</abbr> statements in a file without executing them.
      *
      * @return {@code true}.
      */
@@ -271,217 +294,41 @@ public final class DataScriptFormatter extends ScriptRunner {
     protected int execute(final StringBuilder sql) throws IOException, SQLException {
         removeLF(sql);
         String line = CharSequences.trimWhitespaces(sql).toString();
-        if (line.startsWith("UPDATE ")) {
-            /*
-             * Some EPSG tables have a "table_name" field which will contain the names of other EPSG tables.
-             * In the EPSG scripts, the values are initially the table names used in the MS-Access database.
-             * Then the MS-Access table names are replaced by statements like below:
-             *
-             *    UPDATE epsg_alias SET object_table_name = 'epsg_coordinateaxis' WHERE object_table_name = 'Coordinate Axis';
-             *    UPDATE epsg_deprecation SET object_table_name = 'epsg_alias' WHERE object_table_name = 'Alias';
-             *    etc.
-             *
-             * For Apache SIS, we keep the original table names as defined in MS-Access database,
-             * for consistency with the table names that we actually use in our EPSG schema.
-             */
-            if (line.contains("object_table_name")) {
-                return 0;
-            }
-            /*
-             * Following statements do not make sense anymore on enumerated or boolean values:
-             *
-             *    UPDATE epsg_coordinatereferencesystem SET coord_ref_sys_kind = replace(coord_ref_sys_kind, CHR(182), CHR(10));
-             *    UPDATE epsg_coordinatesystem SET coord_sys_type = replace(coord_sys_type, CHR(182), CHR(10));
-             *    UPDATE epsg_datum SET datum_type = replace(datum_type, CHR(182), CHR(10));
-             *    UPDATE epsg_coordoperationparamusage SET param_sign_reversal = replace(param_sign_reversal, CHR(182), CHR(10))
-             */
-            if (line.contains("replace")) {
-                if (line.contains("param_sign_reversal") || line.contains("coord_ref_sys_kind")
-                        || line.contains("coord_sys_type") || line.contains("datum_type"))
-                {
-                    return 0;
-                }
-            }
-        }
-        if (insertStatement != null) {
-            if (line.startsWith(insertStatement)) {
+        /*
+         * Search for a table in reverse order on the assumption that the most recently added tables
+         * are the most likely to be reused in the next statement.
+         */
+        for (int i = tableCount; --i >= 0;) {
+            final TableValues values = valuesPerTable[i];
+            if (line.startsWith(values.insertStatement)) {
                 // The previous instruction was already an INSERT INTO the same table.
-                out.append(",\n");      // Really want Unix EOL, not the platform-specific one.
-                writeValues(editInsertValues(line));
+                values.add(line);
                 return 1;
             }
-            // Previous instruction was the last INSERT INTO for a given table.
-            // We now have a new instruction. Append the pending cariage return.
-            out.append(";\n");
         }
+        /*
+         * We are beginning insertions in a new table.
+         */
         if (line.startsWith(INSERT_INTO)) {
             int valuesStart = line.indexOf(VALUES, INSERT_INTO.length());
             if (valuesStart < 0) {
                 throw new SQLException("This simple program wants VALUES on the same line as INSERT INTO.");
             }
-            final String table = CharSequences.trimWhitespaces(line, INSERT_INTO.length(), valuesStart).toString();
-            booleanColumnIndices = booleanColumnIndicesForTables.getOrDefault(table, ArraysExt.EMPTY_INT);
-            doubleColumnIndices  =  doubleColumnIndicesForTables.getOrDefault(table, ArraysExt.EMPTY_INT);
-            /*
-             * We are beginning insertions in a new table.
-             */
             valuesStart += VALUES.length();     // Move to the end of "VALUES".
-            insertStatement = CharSequences.trimWhitespaces(line, 0, valuesStart).toString();
-            out.append(insertStatement).append('\n');
-            writeValues(editInsertValues(line));
+            final CharSequence insertStatement = CharSequences.trimWhitespaces(line, 0, valuesStart);
+            final var values = new TableValues(insertStatement.toString(), booleanColumns, doubleColumns, excludedColumns);
+            valuesPerTable[tableCount++] = values;
+            values.add(line);
             return 1;
         }
-        insertStatement = null;
-        if (!omit(line)) {
-            out.append(line).append(";\n");
-        }
+        /*
+         * May be an {@code UPDATE} statement. They existed in older versions of EPSG database.
+         * But with the most recent EPSG versions, the `otherStatements` list should stay empty.
+         */
+        otherStatements.add(line);
         return 0;
     }
 
-    /**
-     * Writes the values after an {@code INSERT INTO "Table" VALUES} expression.
-     * This method tries to remove extra spaces before ( and after ) for producing a more compact file.
-     */
-    private void writeValues(final String values) throws IOException {
-        if (values.startsWith("(") && values.endsWith(")")) {
-            out.append('(').append(CharSequences.trimWhitespaces(values, 1, values.length() - 1)).append(')');
-        } else {
-            out.append(values);
-        }
-    }
-
-    /**
-     * Modifies the given {@code INSERT INTO table VALUE (…)} line before to write it.
-     * The given line is only the {@code VALUE (…)} part for a single entry.
-     * The modifications applied on entry values can be a change of integer types to
-     * boolean types, and rewrite of some floating point values.
-     */
-    private String editInsertValues(String line) throws SQLException {
-        line = CharSequences.trimWhitespaces(line, insertStatement.length(), line.length()).toString();
-        line = editColumns(booleanColumnIndices, line, DataScriptFormatter::replaceIntegerByBoolean);
-        line = editColumns(doubleColumnIndices,  line, DataScriptFormatter::reformatFloatingPoints);
-        line = removeUselessExponents(line);
-        return line;
-    }
-
-    /**
-     * Modifies the content of columns identified by the given indices.
-     *
-     * @param  indices    indices (in reversal order) of columns to edit. Index 0 is the last column,
-     *                    index 1 is the column before the last, <i>etc</i>.
-     * @param  line       the line to modify.
-     * @param  converter  the transformation to apply columns identified by the given indices.
-     *                    A {@code null} return value means that the conversion cannot be performed.
-     * @return the modified line.
-     */
-    private static String editColumns(final int[] indices, final String line, final UnaryOperator<String> converter)
-            throws SQLException
-    {
-        final var buffer = new StringBuilder(line);
-        int end = CharSequences.skipTrailingWhitespaces(buffer, 0, buffer.length());
-        if (buffer.codePointBefore(end) == ')') end--;
-        for (int n=0, columnIndex=0; n < indices.length; columnIndex++) {
-            int start = end;
-            for (int c; (c = buffer.codePointBefore(start)) != ',';) {
-                start -= Character.charCount(c);
-                if (c == '\'') {
-                    while (true) {
-                        c = buffer.codePointBefore(start);
-                        start -= Character.charCount(c);
-                        if (c == '\'') {
-                            if (buffer.codePointBefore(start) != '\'') {
-                                break;
-                            }
-                            start--;
-                        }
-                    }
-                }
-            }
-            if (columnIndex == indices[n]) {
-                final String value = CharSequences.trimWhitespaces(buffer, start, end).toString();
-                final String c = converter.apply(value);
-                if (value == null) {
-                    throw new SQLException("Unexpected value \"" + value + "\" at position " + start + " in:\n" + line);
-                }
-                buffer.replace(start, end, c);
-                n++;
-            }
-            end = CharSequences.skipTrailingWhitespaces(buffer, 0, start - 1);
-        }
-        return buffer.toString();
-    }
-
-    /**
-     * Replaces the last {@code SMALLINT} types by {@code BOOLEAN}.
-     * This is for consistency with the table type documented in the class javadoc.
-     */
-    private static String replaceIntegerByBoolean(String value) {
-        if (value.equals("0") || value.equalsIgnoreCase("'No'")) {
-            value = "false";
-        } else if (value.equals("1") || value.equalsIgnoreCase("'Yes'")) {
-            value = "true";
-        } else if (value.equalsIgnoreCase("Null") || value.equals("''")) {
-            value = "Null";
-        } else {
-            value = null;
-        }
-        return value;
-    }
-
-    /**
-     * Reformats the given floating point number. This is used for replacing for example
-     * {@code -0.000000000000000000000003689471323} by {@code -3.689471323E-24}.
-     */
-    private static String reformatFloatingPoints(String value) {
-        if (!value.equalsIgnoreCase("Null")) {
-            value = Double.toString(Double.parseDouble(value));
-            value = CharSequences.trimFractionalPart(value).toString();
-        }
-        return value;
-    }
-
-    /**
-     * For private usage by the following method only.
-     */
-    private final Pattern uselessExponentPattern =
-            Pattern.compile("([\\(\\,]\\-?\\d+\\.\\d+)E[\\+\\-]?0+([\\,\\)])");
-
-    /**
-     * Removes the useless "E0" exponents after floating point numbers.
-     */
-    private String removeUselessExponents(String line) {
-        StringBuilder cleaned = null;
-        final Matcher matcher = uselessExponentPattern.matcher(line);
-        while (true) {
-            int lastIndex = 0;
-            while (matcher.find()) {
-                // Make sure this is not a quoted text.
-                boolean quoted = false;
-                for (int i=matcher.start(); (i=line.lastIndexOf('\'', i-1)) >= 0;) {
-                    if (i == 0 || line.charAt(i-1) != '\\') {
-                        quoted = !quoted;
-                    }
-                }
-                if (!quoted) {
-                    // Found a number outside quotes. Replace.
-                    if (cleaned == null) {
-                        cleaned = new StringBuilder();
-                    }
-                    cleaned.append(line, lastIndex, matcher.end(1));
-                    lastIndex = matcher.end();
-                    cleaned.append(line, matcher.start(2), lastIndex);
-                }
-            }
-            if (lastIndex == 0) {
-                return line;
-            }
-            cleaned.append(line, lastIndex, line.length());
-            line = cleaned.toString();
-            matcher.reset(line);
-            cleaned.setLength(0);
-        }
-    }
-
     /**
      * Reformats a multi-line text as a single line text. For each occurrence of line feed
      * (the {@code '\n'} character) found in the given buffer, this method performs the following steps:
@@ -499,16 +346,16 @@ public final class DataScriptFormatter extends ScriptRunner {
      * <p><b>Note:</b> current version does not use codepoint API
      * on the assumption that it is not needed for EPSG's SQL files.</p>
      *
-     * @param  buffer  the string in which to perform the removal.
+     * @param  sql  the string in which to perform the removal.
      */
-    static void removeLF(final StringBuilder buffer) {
-        int i = buffer.length();
-        while ((i = buffer.lastIndexOf("\n", i)) >= 0) {
-            final int length = buffer.length();
+    static void removeLF(final StringBuilder sql) {
+        int i = sql.length();
+        while ((i = sql.lastIndexOf("\n", i)) >= 0) {
+            final int length = sql.length();
             int nld = 0;
             int upper = i;
             while (++upper < length) {
-                final char c = buffer.charAt(upper);
+                final char c = sql.charAt(upper);
                 if (!Character.isWhitespace(c)) {
                     if (Character.isLetterOrDigit(c)) {
                         nld++;
@@ -517,7 +364,7 @@ public final class DataScriptFormatter extends ScriptRunner {
                 }
             }
             while (i != 0) {
-                final char c = buffer.charAt(--i);
+                final char c = sql.charAt(--i);
                 if (!Character.isWhitespace(c)) {
                     if (Character.isLetterOrDigit(c)) {
                         nld++;
@@ -529,9 +376,9 @@ public final class DataScriptFormatter extends ScriptRunner {
             if (nld == 2) {
                 upper--;
             }
-            buffer.delete(i, upper);
+            sql.delete(i, upper);
             if (nld == 2) {
-                buffer.setCharAt(i, ' ');
+                sql.setCharAt(i, ' ');
             }
         }
     }
diff --git a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql
index b9aa24ba56..20346befa9 100644
--- a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql
+++ b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql
@@ -6,13 +6,10 @@
 
 --
 -- Creates views for manual inspections of EPSG dataset. This script can be executed after EPSG database
--- installation on PostgreSQL. This file is never read by Apache SIS; it is provided only as a debugging
+-- installation on PostgreSQL. This file is never read by Apache SIS. It is provided only as a debugging
 -- tool when the developer is looking for some information about the EPSG dataset.
 --
 
-SET client_encoding = 'UTF8';
-SET search_path = 'EPSG';
-
 
 --
 -- The dimensions (number of axes) of all Coordinate Reference Systems found in the EPSG database.
@@ -24,13 +21,13 @@ CREATE VIEW "CRS dimension" AS
  RIGHT JOIN "Coordinate Reference System" AS CRS ON CA.COORD_SYS_CODE = CRS.COORD_SYS_CODE
  GROUP BY COORD_REF_SYS_CODE, COORD_REF_SYS_NAME ORDER BY COORD_REF_SYS_CODE;
 
-COMMENT ON VIEW "CRS dimension" IS 'Dimensions of all Coordinate Reference Systems.';
+COMMENT ON VIEW "CRS dimension" IS 'Number of dimensions of all Coordinate Reference Systems.';
 
 
 --
 -- The source and target dimensions of all Operation Methods found in the EPSG database.
 --
-CREATE VIEW "OperationMethod dimension" AS
+CREATE VIEW "Operation Method dimension" AS
  SELECT COM.COORD_OP_METHOD_CODE, COORD_OP_METHOD_NAME, COORD_OP_TYPE, IS_CONVERSION,
         SOURCE_MIN_DIM, SOURCE_MAX_DIM, TARGET_MIN_DIM, TARGET_MAX_DIM
  FROM
@@ -53,20 +50,20 @@ CREATE VIEW "OperationMethod dimension" AS
  RIGHT JOIN "Coordinate_Operation Method" AS COM ON COM.COORD_OP_METHOD_CODE = DIM.COORD_OP_METHOD_CODE
  ORDER BY COM.COORD_OP_METHOD_CODE;
 
-COMMENT ON VIEW "OperationMethod dimension" IS 'Dimensions of most Operation Methods.';
+COMMENT ON VIEW "Operation Method dimension" IS 'Number of dimensions of most Operation Methods.';
 
 
 --
 -- Summary of Operation Method dimensions, grouped by types (projection or not).
 --
-CREATE VIEW "OperationMethodType dimension" AS
+CREATE VIEW "Operation Method Type dimension" AS
  SELECT COORD_OP_TYPE, IS_CONVERSION,
     MIN(OD.SOURCE_MIN_DIM) AS SOURCE_MIN_DIM,
     MAX(OD.SOURCE_MAX_DIM) AS SOURCE_MAX_DIM,
     MIN(OD.TARGET_MIN_DIM) AS TARGET_MIN_DIM,
     MAX(OD.TARGET_MAX_DIM) AS TARGET_MAX_DIM
- FROM "OperationMethod dimension" AS OD
+ FROM "Operation Method dimension" AS OD
  GROUP BY COORD_OP_TYPE, IS_CONVERSION
  ORDER BY IS_CONVERSION;
 
-COMMENT ON VIEW "OperationMethodType dimension" IS 'Dimensions of Operation Methods types.';
+COMMENT ON VIEW "Operation Method Type dimension" IS 'Number of dimensions of Operation Method types.';
diff --git a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/README.md b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/README.md
index 905941e658..0605037233 100644
--- a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/README.md
+++ b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/README.md
@@ -57,13 +57,14 @@ cp $EPSG_SCRIPTS/PostgreSQL_FKey_Script.sql  FKeys.sql
 Open the `Tables.sql` file for edition:
 
 * Keep the header comments that existed in the overwritten file.
+* Remove the `"Change"` table and the `change_id` column in all tables. They are EPSG metadata unused by Apache SIS.
+* Remove the `information_source`, `data_source` and `revision_date` columns in all tables. They are EPSG metadata unused by Apache SIS.
+* Remove the `crs_scope`, `coord_op_scope`, `datum_scope` and `area_of_use_code` columns, which are deprecated.
 * Keep the same column order than in the previous `Tables.sql`.
+* Rename `epsg_` table names to the camel case convention used by Apache SIS.
 * Suppress trailing `NULL` (not to be confused with `NOT NULL`) as they are implicit.
 * In the statement creating the `coordinateaxis` table,
   add the `NOT NULL` constraint to the `coord_axis_code` column.
-* In the statement creating the `change` table,
-  remove the `UNIQUE` constraint on the `change_id` column
-  and add a `CONSTRAINT pk_change PRIMARY KEY (change_id)` line instead.
 * In the statement creating the `epsg_datum` table,
   change the type of the `realization_epoch` and `publication_date` columns to `DATE`.
 * Change the type of `ellipsoid_shape`, `reverse_op`, `param_sign_reversal`
@@ -72,15 +73,17 @@ Open the `Tables.sql` file for edition:
 * Change all `FLOAT` types to `DOUBLE PRECISION` because Apache SIS reads all numbers as `double` type.
   This change avoids spurious digits in the conversions from `float` to `double`.
 * Change the type of `epsg_usage` column from `SERIAL` to `INTEGER NOT NULL`.
-* Change the type of every `table_name` columns from `VARCHAR(80)` to `epsg_table_name`.
-* Change the type of `coord_ref_sys_kind` column from `VARCHAR(24)` to `epsg_crs_kind`.
-* Change the type of `coord_sys_type` column from `VARCHAR(24)` to `epsg_cs_kind`.
-* Change the type of `datum_type` column from `VARCHAR(24)` to `epsg_datum_kind`.
-* Change the type of `supersession_type` column from `VARCHAR(50)` to `epsg_supersession_type`.
+* Change the type of every `table_name` columns from `VARCHAR(80)` to `"Table Name"`.
+* Change the type of `coord_ref_sys_kind` column from `VARCHAR(24)` to `"CRS Kind"`.
+* Change the type of `coord_sys_type` column from `VARCHAR(24)` to `"CS Kind"`.
+* Change the type of `datum_type` column from `VARCHAR(24)` to `"Datum Kind"`.
+* Change the type of `supersession_type` column from `VARCHAR(50)` to `"Supersession Type"`.
+* If new enumeration values are added, check the maximal lengths of `VARCHAR` replacements in `EPSGInstaller`.
 * Suppress trailing spaces and save.
 
 Then open the `FKeys.sql` file for edition:
 
+* Remove the `fk_change_id` foreigner key.
 * At the end of all `ALTER TABLE` statement, append `ON UPDATE RESTRICT ON DELETE RESTRICT`.
 * Suppress trailing spaces and save.
 
@@ -89,7 +92,7 @@ in which case the maintainer can just revert the changes in order to preserve th
 However, if some changes are found in the schema, then hard-coded values in the `DataScriptFormatter` class may need
 to be modified, in particular the `booleanColumnIndicesForTables` and `doubleColumnIndicesForTables` collections.
 
-Execute the `main` method of the `org.apache.sis.referencing.factory.sql.epsg.DataScriptFormatter` class
+Execute the `main` method of the `org.apache.sis.referencing.factory.sql.epsg.Updater` class
 located in the test directory of the `org.apache.sis.non-free:sis-epsg` Maven sub-project.
 Adjust version numbers as needed in the following commands:
 
@@ -107,7 +110,7 @@ export CLASSPATH=$PWD/endorsed/build/classes/java/test/org.apache.sis.metadata:$
 export CLASSPATH=$PWD/optional/build/classes/java/test/org.apache.sis.referencing.epsg:$CLASSPATH
 
 # From any directory
-java org.apache.sis.referencing.factory.sql.epsg.DataScriptFormatter $EPSG_SCRIPTS/PostgreSQL_Data_Script.sql $NON_FREE_DIR/Data.sql
+java org.apache.sis.referencing.factory.sql.epsg.Updater $EPSG_SCRIPTS/PostgreSQL_Data_Script.sql $NON_FREE_DIR/Data.sql
 ```
 
 Run the tests. It it convenient to run `org.apache.sis.referencing.factory.sql.EPSGInstallerTest`
diff --git a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/TableValues.java b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/TableValues.java
new file mode 100644
index 0000000000..815b29541b
--- /dev/null
+++ b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/TableValues.java
@@ -0,0 +1,364 @@
+/*
+ * 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.referencing.factory.sql.epsg;
+
+import java.util.Set;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.sql.SQLException;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.StringBuilders;
+
+
+/**
+ * The values associated to an {@code INSERT INTO "Table"} statement.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ */
+final class TableValues implements Comparator<String> {
+    /**
+     * The {@code INSERT INTO "Table"} statement currently executing, with all columns.
+     * Used in order to detect when the script starts to insert values in a different table.
+     */
+    final String insertStatement;
+
+    /**
+     * All columns found in {@link #insertStatement}.
+     * Excluded columns will be set to {@code null} after all lines have been added.
+     */
+    private final String[] columns;
+
+    /**
+     * Indices (in reverse order) of columns to change from type SMALLINT to type BOOLEAN.
+     * Index 0 is the last column, index 1 is the column before the last, <i>etc</i>.
+     * We use the reverse order because most Boolean columns in the EPSG dataset are last.
+     */
+    private final int[] booleanColumnIndices;
+
+    /**
+     * Indices (in reverse order) of columns to reformat as {@code double} values.
+     * The EPSG database contains values like {@code -0.000000000000000000000003689471323},
+     * which are either not parsed by Derby because too long (Derby 10.14 documents a limit
+     * of 30 characters for floating-point constants) or parsed as 0. To workaround such
+     * limitation, we reformat the above value as {@code -3.689471323E-24}.
+     */
+    private final int[] doubleColumnIndices;
+
+    /**
+     * Indices (in reverse order) of columns to exclude. Some are columns deprecated since <abbr>EPSG</abbr> version 10+,
+     * but still present for compatibility reasons with only null values in every rows. Some other are <abbr>EPSG</abbr>
+     * metadata not used by <abbr>SIS</abbr> (e.g. change records).
+     */
+    private final int[] excludedColumnIndices;
+
+    /**
+     * Whether the whole table is excluded. We exclude <abbr>EPSG</abbr> metadata
+     * that are not used by Apache <abbr>SIS</abbr>, such as the {@code "Change"} table.
+     */
+    private final boolean excludedTable;
+
+    /**
+     * The reformatted values to insert in the table.
+     */
+    private final List<String> data;
+
+    /**
+     * Creates an initially empty set of values.
+     *
+     * @param insertStatement   the {@code INSERT INTO} statement.
+     * @param booleanColumns    names of the columns to change from type SMALLINT to type BOOLEAN.
+     * @param doubleColumns     names of the columns to reformat as {@code double} values.
+     * @param excluded0Columns  names of the columns to exclude.
+     */
+    TableValues(final String      insertStatement,
+                final Set<String> booleanColumns,
+                final Set<String> doubleColumns,
+                final Set<String> excludedColumns)
+    {
+        this.insertStatement = insertStatement;
+        columns = (String[]) CharSequences.split(
+                insertStatement.substring(
+                        insertStatement.indexOf('(') + 1,
+                        insertStatement.lastIndexOf(')')), ',');
+
+        booleanColumnIndices  = indices(booleanColumns);
+        doubleColumnIndices   = indices(doubleColumns);
+        excludedColumnIndices = indices(excludedColumns);
+        excludedTable = insertStatement.contains("\"Change\"");
+        data = new ArrayList<>(1000);
+    }
+
+    /**
+     * Returns the indices where the columns are found, in reverse order.
+     * Indices are returned in reverse order (0 is the last column) for convenience,
+     * as it is slightly easier to modify a line in reverse order (modified parts of
+     * the text do not change the indexes of the next parts to look at).
+     */
+    private int[] indices(final Set<String> search) {
+        final int[] indices = new int[columns.length];
+        int n = 0;
+        for (int i = columns.length; --i >= 0;) {
+            if (search.contains(columns[i])) {
+                indices[n++] = (columns.length - 1) - i;
+            }
+        }
+        return Arrays.copyOfRange(indices, 0, n);
+    }
+
+    /**
+     * Used for sorting rows in increasing order of identifiers.
+     * The identifiers are usually integers, but sometime use a {@code majpor.minor} pattern.
+     */
+    @Override
+    public int compare(final String line1, final String line2) {
+        int s1 = endOfInteger(line1, 0);
+        int s2 = endOfInteger(line2, 0);
+        if (s1 == 0) return (s2 == 0) ? line1.compareTo(line2) : +1;
+        if (s2 == 0) return -1;  // Sort lines without identifier last.
+        int c = Integer.parseInt(line1, 0, s1, 10)
+              - Integer.parseInt(line2, 0, s2, 10);
+        if (c == 0) {
+            final boolean p1 = line1.charAt(s1) == '.';
+            final boolean p2 = line2.charAt(s2) == '.';
+            if (p1 & p2) {
+                // We do not bother to check if a number exists. It should always be the case.
+                c = Integer.parseInt(line1, ++s1, endOfInteger(line1, s1), 10)
+                  - Integer.parseInt(line2, ++s2, endOfInteger(line2, s2), 10);
+            } else {
+                if (p1) return +1;
+                if (p2) return -1;
+            }
+        }
+        return c;
+    }
+
+    /**
+     * Returns the index after the last digit, or {@code offset} if none.
+     *
+     * @param  line    the line to scan.
+     * @param  offset  first index to scan.
+     * @return index after the last digit.
+     */
+    private static int endOfInteger(final String line, int offset) {
+        final int length = line.length();
+        while (offset < length) {
+            final char c = line.charAt(offset);
+            if (c < '0' || c > '9') break;
+            offset++;
+        }
+        return offset;
+    }
+
+    /**
+     * Writes the {@code INSERT INTO} statement followed by all values.
+     * This is invoked for writing the final result after all lines have been added.
+     */
+    final void write(final BufferedWriter out) throws IOException {
+        if (data.isEmpty()) {
+            // The whole table has been excluded. Example: "Change".
+            return;
+        }
+        data.sort(this);
+        for (int i : excludedColumnIndices) {
+            columns[columns.length - 1 - i] = null;
+        }
+        out.write(insertStatement, 0, insertStatement.indexOf('('));
+        String separator = "(";
+        for (String column : columns) {
+            if (column != null) {
+                out.write(separator);
+                out.write(column);
+                separator = ", ";
+            }
+        }
+        separator = insertStatement.substring(insertStatement.lastIndexOf(')')) + '\n';
+        for (String value : data) {
+            out.write(separator);
+            out.write('(');
+            out.write(value);
+            out.write(')');
+            separator = ",\n";      // Really want Unix EOL, not the platform-specific one.
+        }
+        out.write(";\n");
+    }
+
+    /**
+     * Modifies the given {@code INSERT INTO table VALUE (…)} line, then adds it to the list of lines to write.
+     * The given line is only the {@code VALUE (…)} part for a single entry.
+     * The modifications applied on entry values can be a change of integer types to
+     * Boolean types and a reformatting of some floating point values.
+     */
+    final void add(String line) throws SQLException {
+        if (excludedTable) {
+            return;
+        }
+        line = removeUselessExponents(line);
+
+        int start, end;
+        end   = line.length();
+        start = CharSequences.skipLeadingWhitespaces(line, insertStatement.length(), end);
+        end   = CharSequences.skipTrailingWhitespaces(line, start, end);
+        if (line.charAt(start++) != '(' || line.charAt(--end) != ')') {
+            throw new SQLException("Illegal `VALUES` statement:" + System.lineSeparator() + line);
+        }
+        start = CharSequences.skipLeadingWhitespaces (line, start, end);
+        end   = CharSequences.skipTrailingWhitespaces(line, start, end);
+        final var buffer = new StringBuilder(end - start).append(line, start, end);
+
+        end = buffer.length();
+        int excludeIndex = 0;
+        int booleanIndex = 0;
+        int doubleIndex  = 0;
+        for (int columnIndex=0; end > 0; columnIndex++) {       // Index in reverse order.
+            /*
+             * Search in reverse order for the indices where each column starts and ends.
+             * If the value is a character sequence, the range includes the ' quotes.
+             * Escaped quotes ('') are supported.
+             */
+            start = end;
+            for (int c; start > 0 && (c = buffer.codePointBefore(start)) != ',';) {
+                start -= Character.charCount(c);
+                if (c == '\'') {
+                    for (;;) {
+                        c = buffer.codePointBefore(start);
+                        start -= Character.charCount(c);
+                        if (c == '\'') {
+                            if (buffer.codePointBefore(start) != '\'') {
+                                break;
+                            }
+                            start--;
+                        }
+                    }
+                }
+            }
+            /*
+             * Check if the column should be excluded. This code assumes that the column to exclude
+             * is never the first column, because the first column is usually the primary key.
+             * This assumption allows to assume that a coma always exists before the column.
+             */
+            if (excludeIndex < excludedColumnIndices.length && columnIndex == excludedColumnIndices[excludeIndex]) {
+                excludeIndex++;
+                buffer.delete(--start, end);    // Above loop sets `start` to the index immediately after the coma.
+                end = CharSequences.skipTrailingWhitespaces(buffer, 0, start);
+                continue;
+            }
+            /*
+             * Eventually reformat the column value. We replace empty strings by Null because
+             * we replaced some VARCHAR by DATE, and an empty string is an invalid date.
+             */
+            String value = CharSequences.trimWhitespaces(buffer, start, end).toString();
+            if (value.equals("''")) {
+                value = "Null";
+            } else if (booleanIndex < booleanColumnIndices.length && columnIndex == booleanColumnIndices[booleanIndex]) {
+                booleanIndex++;
+                value = replaceIntegerByBoolean(value);
+            } else if (doubleIndex < doubleColumnIndices.length && columnIndex == doubleColumnIndices[doubleIndex]) {
+                doubleIndex++;
+                value = reformatFloatingPoints(value);
+            }
+            // Do the replacement even if the value didn't changed because it may remove spaces.
+            buffer.replace(start, end, value);
+            end = CharSequences.skipTrailingWhitespaces(buffer, 0, start - 1);      // -1 is for skipping the coma.
+        }
+        StringBuilders.trimWhitespaces(buffer, 0, buffer.length());
+        data.add(buffer.toString());
+    }
+
+    /**
+     * Replaces the last {@code SMALLINT} types by {@code BOOLEAN}.
+     * This is for consistency with the table type documented in the class Javadoc.
+     */
+    private static String replaceIntegerByBoolean(String value) throws SQLException {
+        if (value.equals("0") || value.equalsIgnoreCase("'No'")) {
+            return "false";
+        } else if (value.equals("1") || value.equalsIgnoreCase("'Yes'")) {
+            return "true";
+        } else if (value.equalsIgnoreCase("Null") || value.equals("''")) {
+            return "Null";
+        } else {
+            throw new SQLException("Unexpected Boolean value \"" + value + "\".");
+        }
+    }
+
+    /**
+     * Reformats the given floating point number. This is used for replacing for example
+     * {@code -0.000000000000000000000003689471323} by {@code -3.689471323E-24}.
+     */
+    private static String reformatFloatingPoints(String value) {
+        if (!value.equalsIgnoreCase("Null")) {
+            value = Double.toString(Double.parseDouble(value));
+            value = CharSequences.trimFractionalPart(value).toString();
+        }
+        return value;
+    }
+
+    /**
+     * For private usage by the following method only.
+     */
+    private static final Pattern USELESS_EXPONENT =
+            Pattern.compile("([\\(\\,]\\-?\\d+\\.\\d+)E[\\+\\-]?0+([\\,\\)])");
+
+    /**
+     * Removes the useless "E0" exponents after floating point numbers.
+     */
+    private static String removeUselessExponents(String line) {
+        StringBuilder cleaned = null;
+        final Matcher matcher = USELESS_EXPONENT.matcher(line);
+        while (true) {
+            int lastIndex = 0;
+            while (matcher.find()) {
+                // Make sure this is not a quoted text.
+                boolean quoted = false;
+                for (int i=matcher.start(); (i=line.lastIndexOf('\'', i-1)) >= 0;) {
+                    if (i == 0 || line.charAt(i-1) != '\\') {
+                        quoted = !quoted;
+                    }
+                }
+                if (!quoted) {
+                    // Found a number outside quotes. Replace.
+                    if (cleaned == null) {
+                        cleaned = new StringBuilder();
+                    }
+                    cleaned.append(line, lastIndex, matcher.end(1));
+                    lastIndex = matcher.end();
+                    cleaned.append(line, matcher.start(2), lastIndex);
+                }
+            }
+            if (lastIndex == 0) {
+                return line;
+            }
+            cleaned.append(line, lastIndex, line.length());
+            line = cleaned.toString();
+            matcher.reset(line);
+            cleaned.setLength(0);
+        }
+    }
+
+    /**
+     * Returns a string representation for debugging purposes.
+     */
+    @Override
+    public String toString() {
+        return insertStatement;
+    }
+}
diff --git a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/Updater.java b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/Updater.java
new file mode 100644
index 0000000000..5718574c4b
--- /dev/null
+++ b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/Updater.java
@@ -0,0 +1,73 @@
+/*
+ * 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.referencing.factory.sql.epsg;
+
+import java.nio.file.Path;
+import java.sql.Connection;
+
+// Test dependencies
+import org.apache.sis.metadata.sql.TestDatabase;
+
+
+/**
+ * A command-line tool for updating the <abbr>EPSG</abbr> {@code Data.sql} file distributed by Apache <abbr>SIS</abbr>.
+ * The steps to follow are documented in the {@code README.md} file.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class Updater {
+    /**
+     * Do not allow instantiation of this class.
+     */
+    private Updater() {
+    }
+
+    /**
+     * Compacts the {@code *Data_Script.sql} file provided by <abbr>EPSG</abbr>.
+     * This method expects two arguments:
+     *
+     * <ol>
+     *   <li>The file of the <abbr>SQL</abbr> script to convert, which must exist.</li>
+     *   <li>The file where to write the compacted script, which will be overwritten without warning if it exists.</li>
+     * </ol>
+     *
+     * The values of those arguments are typically as below, where the {@code EPSG_SCRIPTS} and {@code NON_FREE_DIR}
+     * environment variables are defined in the {@code README.md} file in the same directory as the source code of
+     * this class:
+     *
+     * <ol>
+     *   <li>{@code $EPSG_SCRIPTS/PostgreSQL_Data_Script.sql}</li>
+     *   <li>{@code $NON_FREE_DIR/epsg/Data.sql}</li>
+     * </ol>
+     *
+     * @param  arguments  the source files and the destination file.
+     * @throws Exception if an error occurred while reading of writing the file.
+     */
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
+    public static void main(String[] arguments) throws Exception {
+        if (arguments.length != 2) {
+            System.err.println("Expected two arguments: source SQL file and target SQL file.");
+            return;
+        }
+        try (TestDatabase db = TestDatabase.create("dummy");
+             Connection c = db.source.getConnection())
+        {
+            final var formatter = new DataScriptFormatter(c);
+            formatter.run(Path.of(arguments[0]), Path.of(arguments[1]));
+        }
+    }
+}