Commits

Ronny Pfannschmidt committed 1ea53af

refactor

Comments (0)

Files changed (4)

micromigrate/backend_base.py

+
+HAS_MIGRATIONS = '''
+    select name, type
+    from sqlite_master
+    where type = "table"
+    and name = "micromigrate_migrations"
+'''
+
+MIGRATIONS_AND_CHECKSUMS = """
+    select
+        name,
+        case
+            when completed = 1
+            then checksum
+            else ':failed to complete'
+        end as checksum
+    from micromigrate_migrations
+"""
+
+
+class BackendBase(object):
+
+    def run_query(self, query):
+        raise NotImplementedError
+
+    def state(self):
+        result = self.run_query(HAS_MIGRATIONS)
+        if result:
+            return {
+                row['name']: row['checksum']
+                for row in self.run_query(MIGRATIONS_AND_CHECKSUMS)
+            }
+
+
+

micromigrate/backend_script.py

+"""
+    migration backend using the sqlite command
+
+    supports transactional migrations
+"""
+import subprocess
+
+from .backend_base import BackendBase
+
+
+MIGRATION_SCRIPT = """
+begin transaction;
+
+create table if not exists micromigrate_migrations (
+    id integer primary key,
+    name unique,
+    checksum,
+    completed default 0
+    );
+
+
+insert into micromigrate_migrations (name, checksum)
+values ("{migration.name}", "{migration.checksum}");
+
+select 'executing' as status;
+
+{migration.sql}
+;
+select 'finalizing' as stats;
+
+update micromigrate_migrations
+set completed = 1
+where name = "{migration.name}";
+
+select 'commiting' as status, "{migration.name}" as migration;
+
+commit;
+"""
+
+
+def runquery(dbname, query):
+    proc = subprocess.Popen(
+        ['sqlite3', '-line', str(dbname), query],
+        stdout=subprocess.PIPE,
+        universal_newlines=True,
+    )
+    out, ignored = proc.communicate()
+    if proc.returncode:
+        raise Exception(proc.returncode, out, ignored)
+
+    chunks = out.split('\n\n')
+    return [
+        dict(x.strip().split(' = ') for x in chunk.splitlines())
+        for chunk in chunks if chunk.strip()
+    ]
+
+def runsqlite(dbname, script):
+    subprocess.check_call([
+        'sqlite3', '-line',
+        str(dbname), script,
+    ])
+
+
+class MigrationError(Exception):
+    pass
+
+
+class ScriptBackend(BackendBase):
+    def __init__(self, dbname):
+        self.dbname = dbname
+
+    def __repr__(self):
+        return '<scriptbackend %r>' % (self.dbname,)
+
+
+    def apply(self, migration):
+        script = MIGRATION_SCRIPT.format(migration=migration)
+        try:
+            runsqlite(self.dbname, script)
+        except subprocess.CalledProcessError:
+            raise MigrationError('migration failed')
+
+    def run_query(self, query):
+        return runquery(self.dbname, query)
+
+    def run_script(self, script):
+        runsqlite(self.dbname, script)
+
+    def results(self, query, params):
+        assert not params, 'params unsupported'
+        return runquery(self.dbname, query)
+

micromigrate/migrate.py

             checksum=self.checksum[:6],
         )
 
+    def can_apply_on(self, state):
+        return (
+            self.after is None or
+            not any(name not in state for name in self.after)
+        )
+
 
 def parse_migration(sql):
     """
     return Migration(**meta)
 
 
-MIGRATION_SCRIPT = """
-begin transaction;;
-
-create table if not exists micromigrate_migrations (
-    id integer primary key,
-    name unique,
-    checksum,
-    completed default 0
-    );
-
-
-insert into micromigrate_migrations (name, checksum)
-values ("{migration.name}", "{migration.checksum}");
-
-select 'executing' as status;
-
-{migration.sql}
-;
-select 'finalizing' as stats;
-
-update micromigrate_migrations
-set completed = 1
-where name = "{migration.name}";
-
-select 'commiting' as status, "{migration.name}" as migration;
-
-commit;
-"""
-
-
-def runsqlite(dbname, script):
-    subprocess.check_call([
-        'sqlite3', '-line',
-        str(dbname), script,
-    ])
-
-
-def runquery(dbname, query):
-    proc = subprocess.Popen(
-        ['sqlite3', '-line', str(dbname), query],
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
-    )
-    out, ignored = proc.communicate()
-    if proc.returncode:
-        raise Exception(proc.returncode)
-
-    chunks = out.split('\n\n')
-    return [
-        dict(x.strip().split(' = ') for x in chunk.splitlines())
-        for chunk in chunks if chunk.strip()
-    ]
-
-
-def push_migration(dbname, migration):
-    script = MIGRATION_SCRIPT.format(migration=migration)
-    try:
-        runsqlite(dbname, script)
-    except subprocess.CalledProcessError:
-        pass
-    return migration_state(dbname)
-
-
-def migration_state(dbname):
-    proc = '''select name, type
-        from sqlite_master
-        where type = "table"
-        and name = "micromigrate_migrations"
-    '''
-    listit = """select
-                name,
-                case
-                    when completed = 1
-                    then checksum
-                    else ':failed to complete'
-                end as checksum
-            from micromigrate_migrations
-        """
-    result = runquery(dbname, proc)
-    if result:
-        return dict(
-            (row['name'], row['checksum'])
-            for row in runquery(dbname, listit))
-
-
 def verify_state(state, migrations):
-    missing = []
+    missing = {}
     for migration in migrations:
         if migration.name in state:
             assert state[migration.name] == migration.checksum
         else:
-            missing.append(migration)
+                missing[migration.name] = migration
 
-    return missing
+        return missing
 
 
-def pick_next_doable(migrations):
-    names = set(x.name for x in migrations)
-    migrations = [
-        mig for mig in migrations
-        if mig.after is None or not (names & mig.after)
-    ]
-    return migrations[0]
+def pop_next_to_apply(migrations):
+    takefrom = migrations.copy()
+    while takefrom:
+        name, item = takefrom.popitem()
+        if item.after is None or not any(x in migrations for x in item.after):
+            migrations.pop(name)
+            return item
 
 
-def iter_next_doable(migrations):
-    while migrations:
-        next_migration = pick_next_doable(migrations)
-        yield next_migration
-        migrations.remove(next_migration)
-
-
-def can_do(migration, state):
-    return (
-        migration.after is None or
-        not any(name not in state for name in migration.after)
-    )
-
-
-def apply_migrations(dbname, migrations):
-    # we put our internal migrations behind the given ones intentionally
-    # this requires that people depend on our own migrations
-    # in order to have theirs work
-    state = migration_state(dbname) or {}
+def apply_migrations(db, migrations):
+    state = db.state() or {}
     missing_migrations = verify_state(state, migrations)
 
-    for migration in iter_next_doable(missing_migrations):
-        assert can_do(migration, state)
-        push_migration(dbname, migration)
+    while missing_migrations:
+        migration = pop_next_to_apply(missing_migrations)
+        assert migration.can_apply_on(state)
+        try:
+            db.apply(migration)
+        except Exception as e:
+            print(migration, 'failed', e)
+            break
         state[migration.name] = migration.checksum
-    return migration_state(dbname)
+    real_state = db.state()
+    if real_state is not None:
+        assert state == real_state
+    return real_state

testing/test_miromigrate.py

 import pytest
 from micromigrate import migrate as mm
-
+from micromigrate.backend_script import ScriptBackend
 
 @pytest.fixture
 def dbname(request, tmpdir):
             ])
     return db
 
+@pytest.fixture
+def db(dbname):
+    return ScriptBackend(dbname)
 
 def test_parse_migration():
     result = mm.parse_migration("-- migration test")
     assert result.after == frozenset(('fun',))
 
 
-def test_push_migration(dbname):
-    state = mm.migration_state(dbname)
+def test_push_migration(db):
+    state = db.state()
     assert state is None
     migration = mm.parse_migration("""
         -- migration test
         fail
         """)
-    mm.push_migration(dbname, migration)
-    state = mm.migration_state(dbname)
+    pytest.raises(Exception, db.apply ,migration)
+    state = db.state()
     assert state is None
 
     migration = mm.parse_migration("""
         -- migration test
         create table test(name);
         """)
-    mm.push_migration(dbname, migration)
-    state = mm.migration_state(dbname)
+    db.apply(migration)
+    state = db.state()
     assert state == {'test': migration.checksum}
 
 
-def test_migration_initial(dbname):
-    state = mm.migration_state(dbname)
+def test_migration_initial(db):
+    state = db.state()
     assert state is None
     migration = mm.parse_migration("""
         -- migration test
         create table test(name);
         """)
-    new_state = mm.apply_migrations(dbname, [migration])
+    new_state = mm.apply_migrations(db, [migration])
     assert len(new_state) == 1
     assert new_state[migration.name] == migration.checksum
 
 
-def test_boken_transaction(dbname):
+def test_boken_transaction(db):
     migration = mm.parse_migration(u"""
         -- migration broke
         create table foo(name unique);
         insert into foo values ('a');
         insert into foo values ('a');
         """)
-    state = mm.apply_migrations(dbname, [migration])
+    state = mm.apply_migrations(db, [migration])
     assert state is None
 
     migration_working = mm.parse_migration(u"""
         create table bar(name unique);
         """)
 
-    state = mm.apply_migrations(dbname, [migration_working, migration])
+    state = mm.apply_migrations(db, [migration_working, migration])
     assert len(state) == 1
     assert state['working'] == migration_working.checksum