Renaming H2 Database Files

H2 (h2database.com) is an open-source pure-Java SQL database. It’s fast,  can be encrypted, can be used in embedded mode, and has other nice features. The only thing I didn’t like when looking for an embedded database for my next Java project was that file extensions are fixed. It’s always “.h2.db” for the page file, “.lock.db” for lock file and so on. What I wanted was the distinct extension that my application would use.

Fortunately this database supports pluggable file systems, which allow not only to create your own storage any way you want but also to “wrap” existing file systems to modify only pieces you don’t like. For example, give files different names.

This is how it works. You select a new schema name, in this example “test”. The URL will look like this: “jdbc:h2:test:./mydatabase” (note the “test:” prefix – it should match the schema name). The you create a subclass on an org.h2.store.fs.FilePath class (from the H2 jar). If it’s a totally new file system then you can inherit it directly from FilePath. In this case it will modify existing functionality so it it’s inherited from  org.h2.store.fs.FilePathWrapper class, which in turn is a subclass of FilePath:

package my.test;

import org.h2.store.fs.FilePath;
import org.h2.store.fs.FilePathWrapper;

public class FilePathTestWrapper extends FilePathWrapper {
  private static final String[][] MAPPING = {
    {".h2.db", ".mydb"}, 
    {".mv.db", ".mymv"},
    {".lock.db", ".mydb.lock"}
  };

  @Override
  public String getScheme() {
    return "test";
  }

  @Override
  public FilePathWrapper wrap(FilePath base) {
    // base.toString() returns base.name
    FilePathTestWrapper wrapper = (FilePathTestWrapper) super.wrap(base);
    wrapper.name = getPrefix() + wrapExtension(base.toString());
    return wrapper;
  }

  @Override
  protected FilePath unwrap(String path) {
    String newName = path.substring(getScheme().length() + 1);
    newName = unwrapExtension(newName);
    return FilePath.get(newName);
  }

  protected static String wrapExtension(String fileName) {
    for (String[] pair : MAPPING) {
      if (fileName.endsWith(pair[1])) {
        fileName = fileName.substring(0, fileName.length() - pair[1].length()) + pair[0];
        break;
      }
    }
    return fileName;
  }

  protected static String unwrapExtension(String fileName) {
    for (String[] pair : MAPPING) {
      if (fileName.endsWith(pair[0])) {
        fileName = fileName.substring(0, fileName.length() - pair[0].length()) + pair[1];
        break;
      }
    }
    return fileName;
  }
}

Before opening a database, application should register this class so the H2 library knows there is a new schema:

 ...
 FilePathTestWrapper wrapper = new FilePathTestWrapper();
 FilePath.register(wrapper);
 ...

This is how it works. When a database is accessed H2 library attaches a default extension (“.h2.db”) and creates an instance of FilePath class. In our case it will be FilePathTestWrapper instance with the path “test:./mydatabase.h2.db”. The getPath() method of FilePathWrapper calls unwrap() method, in which we remove our prefix (“test:”), change file extension, and create FilePath instance with the resulting name. Because there are no more prefixes it will by default be FilePathDisk, that is a regular file but with a new name (“./mydatabase.mydb”). This is the name we’ll see if we list directory content.

The wrap() method performs the reverse operation, it takes base FilePath instance (in our case FilePathDisk with name “./mydatabase.mydb”) and “wraps” it creating FilePathTestWrapper instance with name “test:./mydatabase.h2.db” and original FilePathDisk instance as its base. Small problem here is that “name” in FilePath is protected and there is no accessor for it. And because we’re not in the “org.h2.store.fs” package we cannot access this field directly.  Fortunately toString() method of the FilePath class return the value of “name” field although it’s a bit risky because it’s not documented and can change in the future.

The same class can be used to run an H2 console (a very convenient way to inspect a contents on an H2 database). Instead of using org.h2.tools.Console directly we can create a wrapper around it:

package my.test;

import org.h2.store.fs.FilePath;
import org.h2.tools.Console;

public class H2ConsoleTest {
  public static void main(String[] args) throws Exception {
    FilePathTestWrapper wrapper = new FilePathTestWrapper();
    FilePath.register(wrapper);
    Console.main(args);
  }
}

Or even hardcode database name if we use it just to test out project:

package my.test;

import org.h2.store.fs.FilePath;
import org.h2.tools.Console;

public class H2ConsoleTest {
  public static void main(String[] args) throws Exception {
    FilePathTestWrapper wrapper = new FilePathTestWrapper();
    FilePath.register(wrapper);
    Console.main("-url", "jdbc:h2:test:./mydatabase");
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *