occasionally useful ruby, ubuntu, etc


HTML5 Web SQL Database – Intro to Versioning and Migrations

I've been dabbling a bit with Chrome extensions, which have at their disposal HTML5 localStorage and Web SQL Database. And...the web sql database especially is pretty cool. But one thing was bothering me -- if I have a database on the client, how do I maintain the schema? The (simplest) answer is in changeVersion.

First a little background -- each database has an "version" property stored in the metadata of the database. This basically safeguards you from the following scenario:

  1. Webmaster creates Javascript that requires database with schema A
  2. User goes to website and a local database with schema A is created
  3. Webmaster updates Javascript to use schema B
  4. User goes to another page -- but what now? User's database is using schema A but the javascript expects schema B

How does the webmaster update from A to B? Or create the initial schema, for that matter? changeVersion.

Here's how you migrate the original schema, A in our case:

That works...but already there's a problem. If a user revisits a page, that changeVersion line will attempt to execute again. Easy enough to fix:

(Note: the second argument to openDatabase is the "expected version" of the database. If you specify it, the openDatabase method will fail unless the database matches the version you provided, without chance to recover. I haven't found a use for this yet, so you'll always see the expected version as empty string from me. Also note: this particular problem could be solved by setting the initial schema in the openDatabase callback, but you'd still hit it later when you add your first schema alteration)

Now users can view and refresh a page without the database connection erroring out. Gooood. But wait, I need to modify the schema again. What now? Let's try this:

This...actually doesn't work at all (except within the scope of web workers, perhaps), because within the "regular" web sql database (window.openDatabase), everything is asynchronous. Which is awesome for performance, but a pain if you're trying to do what we're trying to do right now. Also, changeVersion only changes the version after the callback has run, successfully. In other words, for a user new to the site with no schema, the "if db version == 1" line will get executed before the database has been updated to version 1! If you refresh the page a few times this might work, but...that's obviously not acceptable. But we're getting closer:

Alright, this actually works, but it's cumbersome and still has a fair bit of code duplication (less in the second version...).

And now for the punch line -- what if we take that "v3" migration, stash all the migrations in an array, and...say, put it in a class, maybe called Migrator? And that's what I've done: And usage example:

Granted, it is small and fairly simple (and uh, disclaimer, not tested super thoroughly), but if nothing else it should be a good starter for your own migration scripts.

Thanks for reading!

UPDATE on June 5, 2010:
I've created a github project with a more recent version (one that works with Safari/Webkit), available here. It does this by using a schema table to track state instead of relying on changeVersion. While sort of a hack, the changeVersion design is rather poor anyway (pretty much the only useful openDatabase version string is "", since everything else could throw an exception). Additionally, I added some logging, an example, and what I discovered to be a much-needed feature: callbacks, which let you execute further SQL with confidence that the migrations have already occurred.

Tagged as: , Leave a comment
Comments (17) Trackbacks (2)
  1. Thank you, this is definitely useful! I was wondering how web developers expected to update their applications for users with different versions, and you answered it :)

    Now, the annoying question: won’t the code become too big overtime, if you have to embed all the migration scripts?

    • Uh….gzip your javascript? :) That is a valid point. One solution I can think of is to break up your migrations into like groups of 10 or something, then detect which migrations a user needs to download, i.e. if I’m going from schema v15 to v25, I’ll need to download migrations1.js and migrations2.js, but if I’m going from v24 to v25 I only need migrations2.js. Annoying, but I’m sure a system could be worked out for that fairly easily.

  2. Elegant and very useful!

    Sadly enough, Migrator won’t work on iPhone because the new database version number becomes visible in Safari only in future sessions. See last paragraph in: http://developer.apple.com/safari/library/documentation/iphone/conceptual/safarijsdatabaseguide/usingthejavascriptdatabase/usingthejavascriptdatabase.html

    Any attentions on publishing a version that works around that problem? Is it possible to have Migrator check and update the version number only once while executing the entire migrations array?

    • Hmm, that’s pesky. I’ll see if there’s anything I can do about it. I won’t be able to test it on the iPhone, but of course I can test it on Safari for Windows. I’ll send you an email when I update the post.

  3. schory, not only does it not work on iPhone, it also doesn’t work on certain Safari versions because of a bug (id 27836, see https://bugs.webkit.org/show_bug.cgi?id=27836 )

    I’ve found a workaround like this :

    _db.changeVersion(_db.version, version,
    function(tx){ // transaction
    function(err){ // error
    if (err.code == 2) {
    // Safari bug 27836 probably occured
    // changeVersion() will never work on a Safari version with this bug, so we need to do the transaction again, and manually maintain the version number
    _doSafariBugMigration(versions, migration);
    function(){ // success
    JuSy.log(“[Html5Db] Database migrated to version : ” + version);
    _doMigration(versions, migrations);
    ); // end changeVersion()

    I know it’s fighting a symptom instead of solving the problem, but I can’t think of any other way…

    • Robin, it’s worth noting that the Webkit bug you mention was supposedly fixed in October 2009. Naturally, this doesn’t mean that the fix has been picked up by all browsers using the Webkit engine, but the fix will propagate eventually.

  4. Tried to do the same, but catch an “current version of the database and `oldVersion` argument do not match” error. Can’t changeVersion on new database :(

    • Not sure I exactly follow, but I did have problems with changeVersion not working on some Safari/Webkit browsers, so I resorted to maintaining an actual table for schema tracking instead (as mentioned in my note at the bottom).

      If I’m interpreting what you’re saying though, you’d try to open a database with a particular version string, catch the exception if it wasn’t the expected version, and subsequently performing some sort of migration to get it up to the expected version using changeVersion..? If so, seems like that should work, in theory…

  5. Hi, Thanks for information
    Why NOT is this javascript library the rollback if I failed ?

  6. Thank you! I couldn’t find this at first and posted a question on StackOverflow about it. I’ll put your post in as an answer.

  7. Excelent post. I couldnt explain it better :p. Really usefull.
    Thanks for this.

  8. Nice one, just after your article i found one more article which shows how to join all these pieces and setup a todo maker using websql . It is also nice and informative


  9. Thanks a lot for the Migrator. Help me a lot and working very well. I did not find many discussions and exemples on the subject. So very useful!

  10. That is very userful, I decided to use it for my web app, appreciate again :)

  11. I’ve been through the same issues and here what I ended up doing, it could be easily improved but it’s pretty straight-forward as it is.


  12. Thanks! This saved me from doing it myself (although I already went halfway before searching, oops). Still useful after a few years. :)


  13. In my library https://bitbucket.org/ytkyaw/ydn-db I reflect the existing schema and
    migrate as necessary in one go. It is very effective, although the logic is very complicated.

Leave a comment