Southのマイグレーションのマージ

Mercurialなどのバージョン管理システムでブランチを使っていて、DjangoとSouthを使っている場合、Southのマイグレーションがブランチ間で衝突して、マージしないといけないことがよくある。
その際の解消手順についてメモ。
試したのはDjango1.4.1, South0.7.6
myappにモデルを追加するところから、マイグレーションが衝突するフローとマージまで。

マイグレーションの初期化

モデルがない状態でとりあえず初期化する。

$ python manage.py schemamigration myapp --initial
Created 0001_initial.py. You can now apply this migration with: ./manage.py migrate myapp

syncdbとmigrateを実行する。

$ python manage.py syncdb
$ python manage.py migrate myapp
Running migrations for myapp:
 - Migrating forwards to 0001_initial.
 > myapp:0001_initial
 - Loading initial data for myapp.
Installed 0 object(s) from 0 fixture(s)

ここからモデルを作成していく。

モデルの追加

myapp/models.py

idのみのItemモデルを定義。

from django.db import models


class Item(models.Model):
    class Meta:
        db_table = 'item'

schemamigrationを実行してマイグレーションファイルを追加し、migrateを実行。

$ python manage.py schemamigration myapp --auto
 + Added model myapp.Item
Created 0002_auto__add_item.py. You can now apply this migration with: ./manage.py migrate myapp
$ python manage.py migrate myapp
Running migrations for myapp:
 - Migrating forwards to 0002_auto__add_item.
 > myapp:0002_auto__add_item
 - Loading initial data for myapp.
Installed 0 object(s) from 0 fixture(s)

この状態でソースコードリポジトリにコミットする。コミットしたのはdefaultブランチ。

$ hg add
$ hg commit

ブランチを作成してモデルにフィールドを追加

defaultから作業用にブランチを作成する(issue-123ブランチ)。

$ hg branch issue-123

Itemモデルにnameというフィールドを追加する。

myapp/models.py
from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=20)  # 追加

    class Meta:
        db_table = 'item'

追加したら、マイグレーションを実行する。

$ python manage.py schemamigration myapp --auto
 ? The field 'Item.name' does not have a default specified, yet is NOT NULL.
 ? Since you are adding this field, you MUST specify a default
 ? value to use for existing rows. Would you like to:
 ?  1. Quit now, and add a default to the field in models.py
 ?  2. Specify a one-off value to use for existing columns now
 ? Please select a choice: 2
 ? Please enter Python code for your one-off default value.
 ? The datetime module is available, so you can do e.g. datetime.date.today()
 >>> ''
 + Added field name on myapp.Item
Created 0003_auto__add_field_item_name.py. You can now apply this migration with: ./manage.py migrate myapp
$ python manage.py migrate myapp
Running migrations for myapp:
 - Migrating forwards to 0003_auto__add_field_item_name.
 > myapp:0003_auto__add_field_item_name
 - Loading initial data for myapp.
Installed 0 object(s) from 0 fixture(s)

この状態でコミットする。

defaultブランチでフィールドを追加する

ここで、別の優先される作業をdefaultで行なうとする。nameフィールドの追加より前に別のフィールドが増えるとする。
まず、defaultに切り替える前に、defaultブランチの状態までマイグレーションを実行しておく。

$ python manage.py migrate --list

 myapp
  (*) 0001_initial
  (*) 0002_auto__add_item
  (*) 0003_auto__add_field_item_name

$ python manage.py migrate myapp 0002
 - Soft matched migration 0002 to 0002_auto__add_item.
Running migrations for myapp:
 - Migrating backwards to just after 0002_auto__add_item.
 < myapp:0003_auto__add_field_item_name

ブランチをdefaultに切り替える。

$ hg update default
1 files updated, 0 files merged, 1 files removed, 0 files unresolved

Itemモデルにフィールドを追加する。

myapp/models.py
from django.db import models


class Item(models.Model):
    value = models.IntegerField(default=0)

    class Meta:
        db_table = 'item'

マイグレーションを実行する。

$ python manage.py schemamigration myapp --auto
 + Added field value on myapp.Item
Created 0003_auto__add_field_item_value.py. You can now apply this migration with: ./manage.py migrate myapp
$ python manage.py migrate myapp --list

 myapp
  (*) 0001_initial
  (*) 0002_auto__add_item
  ( ) 0003_auto__add_field_item_value

$ python manage.py migrate myapp
Running migrations for myapp:
 - Migrating forwards to 0003_auto__add_field_item_value.
 > myapp:0003_auto__add_field_item_value
 - Loading initial data for myapp.
Installed 0 object(s) from 0 fixture(s)

この状態でdefaultブランチにコミットする。

$ hg add
$ hg commit

この時点で履歴はこうなっている。

$ hg glog -l 3
@  changeset:   4:248b48490ff8
|  tag:         tip
|  parent:      2:f9731044b980
|  user:        Shinya Okano<tokibito@example.com>
|  date:        Sat Oct 06 23:42:21 2012 +0900
|  summary:     valueフィールドを追加
|
| o  changeset:   3:42ee06323dd9
|/   branch:      issue-123
|    user:        Shinya Okano<tokibito@example.com>
|    date:        Sat Oct 06 23:23:24 2012 +0900
|    summary:     nameフィールドを追加
|
o  changeset:   2:f9731044b980
|  user:        Shinya Okano<tokibito@example.com>
|  date:        Sat Oct 06 23:16:22 2012 +0900
|  summary:     Itemモデルを追加

ブランチをマージする

issue-123ブランチのコミット後にdefaultにコミットがあったため、issue-123のほうでdefaultを取り込む。
まずは、issue-123のブランチ開始時点までマイグレーションしてからブランチを切り替え、issue-123側の最新までマイグレーションする。

$ python manage.py migrate myapp 0002
 - Soft matched migration 0002 to 0002_auto__add_item.
Running migrations for myapp:
 - Migrating backwards to just after 0002_auto__add_item.
 < myapp:0003_auto__add_field_item_value
$ hg update issue-123
2 files updated, 0 files merged, 1 files removed, 0 files unresolved
$ python manage.py migrate myapp
Running migrations for myapp:
 - Migrating forwards to 0003_auto__add_field_item_name.
 > myapp:0003_auto__add_field_item_name
 - Loading initial data for myapp.
Installed 0 object(s) from 0 fixture(s)

ブランチをマージする。同じ行を変更していたので衝突する。

$ hg merge default
merging myapp/models.py
warning: conflicts during merge.
merging myapp/models.py incomplete! (edit conflicts, then use 'hg resolve --mark')
1 files updated, 0 files merged, 0 files removed, 1 files unresolved
use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
(django-1.4)tokibito@ubuntu:~/sandbox/myproject$ hg resolve -l
U myapp/models.py

モデルを編集して衝突を解消し、コミットする。

myapp/models.py
from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=20)
    value = models.IntegerField(default=0)

    class Meta:
        db_table = 'item'
$ hg resolve -m myapp/models.py
$ hg ci

マージ後にmigrateを実行しておく。

$ python manage.py migrate myapp --list

 myapp
  (*) 0001_initial
  (*) 0002_auto__add_item
  (*) 0003_auto__add_field_item_name
  ( ) 0003_auto__add_field_item_value

$ python manage.py migrate myapp
Running migrations for myapp:
 - Migrating forwards to 0003_auto__add_field_item_value.
 > myapp:0003_auto__add_field_item_value
 - Loading initial data for myapp.
Installed 0 object(s) from 0 fixture(s)

さらにブランチでフィールドを追加する(問題発生箇所)

issue-123ブランチで更にItemモデルにフィールドを追加する。

myapp/models.py
from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=20)
    value = models.IntegerField(default=0)
    cost = models.IntegerField(default=0)

    class Meta:
        db_table = 'item'

schemamigrationを実行する。

$ python manage.py schemamigration myapp --auto
 ? The field 'Item.name' does not have a default specified, yet is NOT NULL.
 ? Since you are adding this field, you MUST specify a default
 ? value to use for existing rows. Would you like to:
 ?  1. Quit now, and add a default to the field in models.py
 ?  2. Specify a one-off value to use for existing columns now
 ? Please select a choice:

ここで、ようやく問題が発生する。既にマイグレーションファイルが存在するItem.nameが追加されたフィールドとして検出されてしまう。
これは、最後に適用した0003_auto__add_field_item_valueで、nameフィールドが存在しないことになっているため。
この場合、Southの--emptyオプションでマイグレーションファイルを作成し、存在するフィールドの状態にマイグレーションのデータベースを合わせればよいらしい。
追加するフィールドを一度コメントアウトし、--emptyオプションを指定してschemamigrationを実行する。

myapp/models.py
from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=20)
    value = models.IntegerField(default=0)
    # cost = models.IntegerField(default=0) コメントアウト

    class Meta:
        db_table = 'item'
$ python manage.py schemamigration myapp merge_item_model --empty
Created 0004_merge_item_model.py. You must now edit this migration and add the code for each direction.
$ python manage.py migrate myapp
Running migrations for myapp:
 - Migrating forwards to 0004_merge_item_model.
 > myapp:0004_merge_item_model
 - Loading initial data for myapp.
Installed 0 object(s) from 0 fixture(s)

migrateを実行してから、追加するフィールドのコメントアウトを外し、再度schemamigration, migrateを実行する。

myapp/models.py
from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=20)
    value = models.IntegerField(default=0)
    cost = models.IntegerField(default=0)

    class Meta:
        db_table = 'item'
$ python manage.py schemamigration myapp --auto
 + Added field cost on myapp.Item
Created 0005_auto__add_field_item_cost.py. You can now apply this migration with: ./manage.py migrate myapp
$ python manage.py migrate myapp
Running migrations for myapp:
 - Migrating forwards to 0005_auto__add_field_item_cost.
 > myapp:0005_auto__add_field_item_cost
 - Loading initial data for myapp.
Installed 0 object(s) from 0 fixture(s)

これでフィールドを追加できた。
defaultにマージ後、マイグレーション実行時にInconsistentMigrationHistory例外が発生する場合は、--mergeオプションを指定してmigrateを実行すればよい。

まとめ

異なるブランチで同じアプリケーションのモデルを変更した際のマイグレーションで問題が起こる例とその解消手順について説明した。
結論としては自動検出がおかしくなったら、--emptyで作れ、適用時は--mergeを使え、というところか。
このあたりの話は、Southのドキュメントにも説明があるので目を通しておくのがよいと思う。
この例で説明はしていないが、0003のマイグレーションが2つある状態になっていることに注意。ファイル名によっては実行される順番が前後する可能性がある。
これを解消したい場合は、ブランチのマージ時にマイグレーションのファイル名を適切な名前に変更しておけばよい。