Registering Custom Databases Issue

Hi,
Within the changelog I defined the following :

           
When I try to update the database, the CreateIndexGenerator.generateSql(...) method generates the following sql which does not work with mysql database :
    CREATE UNIQUE INDEX `schemaName`.`inxName` ON `schemaName`.`tableName`(`someField`)
It should be like this :
    CREATE UNIQUE INDEX `inxName` ON `schemaName`.`tableName`(`someField`)

I am not sure it is the best way to do it but I tried to extend the liquibase.database.AbstractDatabase.escapeIndexName(java.lang.String, java.lang.String) method in order to make sure the schema name would not be specified for the index name. Here is the class definition (note that the package is the one specified into the wiki documentation) :

    package liquibase.database.ext;

    public class MySQLDatabase extends liquibase.database.core.MySQLDatabase {

     /* (non-Javadoc)
      * @see liquibase.database.AbstractDatabase#escapeIndexName(java.lang.String, java.lang.String)
      */
     @Override
     public String escapeIndexName(String schemaName, String indexName) {
       return escapeDatabaseObject(indexName);
     }

    }

After packaging and putting the jar into the liquibase/lib folder, this classe is not used and the AbstractDatabase#escapeIndexName(String,String) is still used. I had to move the class into the liquibase.ext package to make sure that my version would be called. Even like this, it is not always my version which is invoked; sometime my class is invoked and some other time, the original liquibase.database.core.MySQLDatabase is.

Could anyone tell me what I did wrong ?

Note : I use liquibase version 2.0 RC2

Hi,
I think I have an explanation for this. Within the liquibase.database.DatabaseFactory class, we can see the following two methods :

       protected DatabaseFactory() {        try {            Class[] classes = ServiceLocator.getInstance().findClasses(Database.class);

               for (Class<? extends Database> clazz : classes) {
                   register(clazz.getConstructor().newInstance());
               }
           } catch (Exception e) {
               throw new RuntimeException(e);
           }
       }

       public Database findCorrectDatabaseImplementation(DatabaseConnection connection) throws DatabaseException {
           Database database = null;

           boolean foundImplementation = false;

           for (Database implementedDatabase : getImplementedDatabases()) {
               database = implementedDatabase;
               if (database.isCorrectDatabaseImplementation(connection)) {
                   foundImplementation = true;
                   break;
               }
           }

           if (!foundImplementation) {
               LogFactory.getLogger().warning("Unknown database: " + connection.getDatabaseProductName());
               database = new UnsupportedDatabase();
           }

           Database returnDatabase;
           try {
               returnDatabase = database.getClass().newInstance();
           } catch (Exception e) {
               throw new UnexpectedLiquibaseException(e);
           }
           returnDatabase.setConnection(connection);
           return returnDatabase;
       }

First, the ServiceLocator.findClasses(Database.class) method returns all the instances of Database.class, even those extended. So the Class[] object returned by this method contains the original liquibase.database.core.MySQLDatabase as well as my extension : liquibase.ext.MySQLDatabase. The DatabaseFactory.findCorrectDatabaseImplementation(DatabaseConnection) methods returns the first found within the list.

Now lets take a look at the last part of the ServiceLocator.findClasses(Database.class) method :

     public Class[] findClasses(Class requiredInterface) throws ServiceNotFoundException {

       //… code to find the implementations
       
       List classes = classesBySuperclass.get(requiredInterface);
       HashSet uniqueClasses = new HashSet(classes);
       return uniqueClasses.toArray(new Class[uniqueClasses.size()]);
     }

And here is an extract of the description of the HashSet class : This class implements the Set interface, backed by a hash table (actually a HashMap instance). It makes no guarantees as to the iteration order of the set; in particular, it does not guarantee that the order will remain constant over time.

Since the list of implementation returned by the ServiceLocator.findClasses(Database.class) contains the original MySQLDatabase implementation as well as my extension and those two implementations are listed in a random order from call to call, this explains why the implementation used is not always the same.

I guess this issue applies to all extensions (TypeConverter, SnapShotGenerator, Executor, etc). IMHO, I see two way in order to avoid this issue :

  • Change the HashSet for another Collection (LinkedHashSet for exemple)
  • Make sure that extensions from **.ext packages are forced to be at the begining of the list.

Another good idea would be to place the liquibase.ext at the begining of the list within the MANIFEST.MF’s Liquibase-Package attibute.

Any confirmation or suggestion ?

I’ll look into it.  I thought we were using a SortedSet rather than a HashSet, so the first entry was always the one with the highest priority.  What you are doing should be correct. 

As a side note, I did change MysqlDatabase to not add the indexName, so grabbing the latest build from http://liquibase.org/ci/latest will stop you from running into the problem.

Nathan