I've been looking into writing database migrations in Clojure, and I
happened across Lobos, which in general looks excellent and a great
complement to ClojureQL. However, I'm a little uncertain about the
proposed migration syntax.
The migration syntax in the README mirrors the Rails method of putting
each migration in its own timestamped file. But this requires that the
migration files exist on the file-system; it doesn't seem like it
would work if the migrations were all bundled into a jar or war, since
there is no interface for searching through packaged resources.
Is there a reason for having one migration per file? Might it not be
more flexible if migrations were placed in a single file, and executed
in the order they are defined? e.g.
(defmigration create-users
(create
(table :users
(integer :id :primary-key)
(varchar :name 100)))
(defmigration add-user-name-length-check
(alter (table :users (check :name (> (length :name) 1)))))
Or am I missing some crucial problem with this?
Good idea. I'd overlooked the fact that migrations might need to be undone.
> Also, I wonder if it would be a good idea to add support for implicit
> rollbacks, that is if the :undo part is missing generate it on the fly or
> maybe it would be safer to force explicit rollbacks and throw an exception
> then.
I'd be tempted to make the :undo part explicit, i.e. raise an
exception if a migration is created without an :undo.
>> Is there a reason for having one migration per file? Might it not be
>> more flexible if migrations were placed in a single file, and executed
>> in the order they are defined? e.g.
>
> To be honest, I've done it that way only because that's what I'm used to
> with Rails. Putting all migrations into one file would certainly be more
> flexible and I don't see any drawback for now. I'll try to find out why
> ActiveRecord works like that.
I'm not quite sure why Rails does it that way, either. Possibly to
make it easier to generate migrations using scripts?
- James
Wouldn't the auto-generated version number be different each time the
migration file was evaluated? Or am I misunderstanding?
- James
Wouldn't the auto-generated version number be different each time the
migration file was evaluated? Or am I misunderstanding?
Why not just rely on the migration name? In Rails, migrations have
timestamps to ensure uniqueness and order, but in Clojure symbol
bindings are unique and ordered anyway.
For example:
(defmigration create-user
(:do [db] ...)
(:undo [db] ...))
(defmigration add-user-creation-date
(:do [db] ...)
(:undo [db] ...))
(An explicit "db" argument seems more in line with how protocols and types work)
If we tried to add another "create-user" definition in the same
namespace, we'd get a warning or error message telling use a symbol
has been redefined. We also know that Clojure evaluates files
linearly, so create-user will always be evaluated before
add-user-creation-date.
So perhaps behind the scenes, a migration like this:
(defmigration create-user
(:do [db] ...)
(:undo [db] ...))
Might get turned into code like:
(do
(defonce lobos-migrations (atom []))
(def create-user
(reify Migration
(migrate-do [db] ...)
(migrate-undo [db] ...)))
(swap! lobos-migrations conj 'create-user))
Then we have have an atom that contains all of the migrations that
*should* be applied, and we can compare this to a database table that
contains all the migrations that *are* applied.
I believe Rails applies migrations out of order if there is a merge
conflict. For example, if there are migrations A and B, and Alice
creates migration C, then she will have a database created by
migrations ABC. But if Bob creates migration D at the same time, then
he'll have a database created by migrations ABD.
Rails resolves this conflict by assuming that migrations are
independent and can be applied out of order. So Alice will wind up
with a database created by ABCD, and Bob will have a database created
by ABDC. I guess the idea is that this method preserves the data in
the development database, because there's no need to undo a migration
that might result in data loss.
However, my feeling is that I'd prefer migrations to applied in the
exact order they will be in production, even if it means my
development database might lose some data. So personally, I'd prefer
the migration library to automatically undo D in Bob's database, and
then run migrations C and D in the correct order.
What do you think?
- James
Why not just rely on the migration name? In Rails, migrations have
timestamps to ensure uniqueness and order, but in Clojure symbol
bindings are unique and ordered anyway.
However, my feeling is that I'd prefer migrations to applied in the
exact order they will be in production, even if it means my
development database might lose some data. So personally, I'd prefer
the migration library to automatically undo D in Bob's database, and
then run migrations C and D in the correct order.
What do you think?
(defmigration add-user-creation-date
(:do [db] ...)
(:undo [db] ...))
(An explicit "db" argument seems more in line with how protocols and types work)
(do
(defonce lobos-migrations (atom []))
(def create-user
(reify Migration
(migrate-do [db] ...)
(migrate-undo [db] ...)))
(swap! lobos-migrations conj 'create-user))
On Sun, May 15, 2011 at 10:27 AM, James Reeves <jre...@weavejester.com> wrote:The way I see it, is to simply let the actions use whatever db connection they have access to. It would be the responsibility of the user to call the migration commands in the proper context. This could enable migrations that can apply changes to any number of different databases.(defmigration add-user-creation-date
(:do [db] ...)
(:undo [db] ...))
(An explicit "db" argument seems more in line with how protocols and types work)
Well, the database configuration can be passed to the migration via a
binding or an explicit argument. An explicit argument might be a
little better in this case, because you might want to use other SQL
libraries in the migration, and a binding would only work for Lodos
functions.
For instance, perhaps you have a database filled with plaintext
passwords and you want to encrypt them with a salted one-way hash. In
this case, you'd probably need to use a library like ClojureQL to
iterate through the table and encrypt all the existing passwords.
- James