Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,6 @@ multiple times, so try not to do that.
If a migration needs to be rolled back,
```spanner-orm rollback <migration_name> <Spanner instance> <Spanner database>```
or the corresponding ```MigrationExecutor``` method should be used.

To see a list of all migrations found, run ```spanner-orm showmigrations <Spanner instance> <Spanner database>```.
Migrations that have already been applied migrations are marked by an `[X]`.
6 changes: 6 additions & 0 deletions spanner_orm/admin/migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ class Migration:
def __init__(self,
migration_id: str,
prev_migration_id: Optional[str],
description: str,
upgrade: Optional[Callable[[], update.SchemaUpdate]] = None,
downgrade: Optional[Callable[[], update.SchemaUpdate]] = None):
self._id = migration_id
self._description = description
self._prev = prev_migration_id
self._upgrade = upgrade or no_update_callable
self._downgrade = downgrade or no_update_callable
Expand All @@ -44,6 +46,10 @@ def migration_id(self) -> str:
def prev_migration_id(self) -> Optional[str]:
return self._prev

@property
def description(self) -> str:
return self._description

@property
def upgrade(self) -> Optional[Callable[[], update.SchemaUpdate]]:
return self._upgrade
Expand Down
14 changes: 13 additions & 1 deletion spanner_orm/admin/migration_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ def migrate(self, target_migration: Optional[str] = None) -> None:
self._update_status(migration_.migration_id, True)
self._hangup()

def show_migrations(self) -> None:
"""Prints information about all migrations.
"""
self._connect()
self._validate_migrations()

for migration_ in reversed(self.migrations()):
migrated = self.migrated(migration_.migration_id)
print('[{}] {}, {}'.format('X' if migrated else ' ', migration_.migration_id, migration_.description))

self._hangup()

def rollback(self, target_migration: str) -> None:
"""Rolls back migrated migrations on the curent database.

Expand Down Expand Up @@ -117,7 +129,7 @@ def _hangup(self) -> None:

def _filter_migrations(
self, migrations: Iterable[migration.Migration], migrated: bool,
last_migration: Optional[str]) -> List[migration.Migration]:
last_migration: Optional[str] = None) -> List[migration.Migration]:
"""Filters the list of migrations according to the desired conditions.

Args:
Expand Down
6 changes: 6 additions & 0 deletions spanner_orm/admin/migration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,15 @@ def _migration_from_file(self, filename: str) -> migration.Migration:
spec = importlib.util.spec_from_file_location(module_name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module_doc = module.__doc__.split('\n')
if not module_doc:
description = "<unknown>"
else:
description = module_doc[0]
try:
result = migration.Migration(module.migration_id,
module.prev_migration_id,
description,
getattr(module, 'upgrade', None),
getattr(module, 'downgrade', None))
except AttributeError:
Expand Down
17 changes: 15 additions & 2 deletions spanner_orm/admin/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ def migrate(args: Any) -> None:
executor.migrate(args.name)


def show_migrations(args: Any) -> None:
connection = api.SpannerConnection(args.instance, args.database)
executor = migration_executor.MigrationExecutor(connection, args.directory)
executor.show_migrations()


def rollback(args: Any) -> None:
connection = api.SpannerConnection(args.instance, args.database)
executor = migration_executor.MigrationExecutor(connection, args.directory)
Expand All @@ -56,14 +62,21 @@ def main(as_module: bool = False) -> None:
generate_parser.set_defaults(execute=generate)

migrate_parser = subparsers.add_parser(
'migrate', help='Execute unmigrated migrations')
'migrate', help='Execute unmigrated migrations')
migrate_parser.add_argument(
'--name', help='Stop migrating after this migration')
'--name', help='Stop migrating after this migration')
migrate_parser.add_argument('--directory')
migrate_parser.add_argument('instance', help='Name of Spanner instance')
migrate_parser.add_argument('database', help='Name of Spanner database')
migrate_parser.set_defaults(execute=migrate)

show_migrations_parser = subparsers.add_parser(
'showmigrations', help='List migrations')
show_migrations_parser.add_argument('--directory')
show_migrations_parser.add_argument('instance', help='Name of Spanner instance')
show_migrations_parser.add_argument('database', help='Name of Spanner database')
show_migrations_parser.set_defaults(execute=show_migrations)

rollback_parser = subparsers.add_parser(
'rollback', help='Roll back migrated migrations')
rollback_parser.add_argument(
Expand Down
91 changes: 54 additions & 37 deletions spanner_orm/tests/migrations_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from io import StringIO
import logging
import os
import shutil
Expand Down Expand Up @@ -66,58 +67,58 @@ def test_generate(self):
shutil.rmtree(self.TEST_MIGRATIONS_DIR)

def test_order_migrations(self):
first = migration.Migration('1', None)
second = migration.Migration('2', '1')
third = migration.Migration('3', '2')
first = migration.Migration('1', None, '1')
second = migration.Migration('2', '1', '2')
third = migration.Migration('3', '2', '3')
migrations = [third, first, second]
expected_order = [first, second, third]

manager = migration_manager.MigrationManager(self.TEST_MIGRATIONS_DIR)
self.assertEqual(manager._order_migrations(migrations), expected_order)

def test_order_migrations_with_no_none(self):
first = migration.Migration('2', '1')
second = migration.Migration('3', '2')
third = migration.Migration('4', '3')
first = migration.Migration('2', '1', '2')
second = migration.Migration('3', '2', '3')
third = migration.Migration('4', '3', '4')
migrations = [third, first, second]
expected_order = [first, second, third]

manager = migration_manager.MigrationManager(self.TEST_MIGRATIONS_DIR)
self.assertEqual(manager._order_migrations(migrations), expected_order)

def test_order_migrations_error_on_unclear_successor(self):
first = migration.Migration('1', None)
second = migration.Migration('2', '1')
third = migration.Migration('3', '1')
first = migration.Migration('1', None, '1')
second = migration.Migration('2', '1', '2')
third = migration.Migration('3', '1', '3')
migrations = [third, first, second]

manager = migration_manager.MigrationManager(self.TEST_MIGRATIONS_DIR)
with self.assertRaisesRegex(error.SpannerError, 'unclear successor'):
manager._order_migrations(migrations)

def test_order_migrations_error_on_unclear_start_migration(self):
first = migration.Migration('1', None)
second = migration.Migration('3', '2')
first = migration.Migration('1', None, '1')
second = migration.Migration('3', '2', '3')
migrations = [first, second]

manager = migration_manager.MigrationManager(self.TEST_MIGRATIONS_DIR)
with self.assertRaisesRegex(error.SpannerError, 'no valid previous'):
manager._order_migrations(migrations)

def test_order_migrations_error_on_circular_dependency(self):
first = migration.Migration('1', '3')
second = migration.Migration('2', '1')
third = migration.Migration('3', '2')
first = migration.Migration('1', '3', '1')
second = migration.Migration('2', '1', '2')
third = migration.Migration('3', '2', '3')
migrations = [third, first, second]

manager = migration_manager.MigrationManager(self.TEST_MIGRATIONS_DIR)
with self.assertRaisesRegex(error.SpannerError, 'No valid migration'):
manager._order_migrations(migrations)

def test_order_migrations_error_on_no_successor(self):
first = migration.Migration('1', None)
second = migration.Migration('2', '3')
third = migration.Migration('3', '2')
first = migration.Migration('1', None, '1')
second = migration.Migration('2', '3', '2')
third = migration.Migration('3', '2', '3')
migrations = [third, first, second]

manager = migration_manager.MigrationManager(self.TEST_MIGRATIONS_DIR)
Expand All @@ -129,9 +130,9 @@ def test_filter_migrations(self):
executor = migration_executor.MigrationExecutor(
connection, self.TEST_MIGRATIONS_DIR)

first = migration.Migration('1', None)
second = migration.Migration('2', '1')
third = migration.Migration('3', '2')
first = migration.Migration('1', None, '1')
second = migration.Migration('2', '1', '2')
third = migration.Migration('3', '2', '3')
migrations = [first, second, third]

migrated = {'1': True, '2': False, '3': False}
Expand All @@ -150,9 +151,9 @@ def test_filter_migrations_error_on_bad_last_migration(self):
executor = migration_executor.MigrationExecutor(
connection, self.TEST_MIGRATIONS_DIR)

first = migration.Migration('1', None)
second = migration.Migration('2', '1')
third = migration.Migration('3', '2')
first = migration.Migration('1', None, '1')
second = migration.Migration('2', '1', '2')
third = migration.Migration('3', '2', '3')
migrations = [first, second, third]

migrated = {'1': True, '2': False, '3': False}
Expand All @@ -168,9 +169,9 @@ def test_validate_migrations(self):
executor = migration_executor.MigrationExecutor(
connection, self.TEST_MIGRATIONS_DIR)

first = migration.Migration('1', None)
second = migration.Migration('2', '1')
third = migration.Migration('3', '2')
first = migration.Migration('1', None, '1')
second = migration.Migration('2', '1', '2')
third = migration.Migration('3', '2', '3')
with mock.patch.object(executor, 'migrations') as migrations:
migrations.return_value = [first, second, third]

Expand All @@ -187,9 +188,9 @@ def test_validate_migrations_error_on_unmigrated_after_migrated(self):
executor = migration_executor.MigrationExecutor(
connection, self.TEST_MIGRATIONS_DIR)

first = migration.Migration('1', None)
second = migration.Migration('2', '1')
third = migration.Migration('3', '2')
first = migration.Migration('1', None, '1')
second = migration.Migration('2', '1', '2')
third = migration.Migration('3', '2', '3')
with mock.patch.object(executor, 'migrations') as migrations:
migrations.return_value = [first, second, third]

Expand All @@ -208,7 +209,7 @@ def test_validate_migrations_error_on_unmigrated_first(self):
executor = migration_executor.MigrationExecutor(
connection, self.TEST_MIGRATIONS_DIR)

first = migration.Migration('2', '1')
first = migration.Migration('2', '1', '2')
with mock.patch.object(executor, 'migrations') as migrations:
migrations.return_value = [first]

Expand All @@ -225,26 +226,42 @@ def test_validate_migrations_error_on_unmigrated_first(self):
def test_migrate(self):
connection = mock.Mock()
executor = migration_executor.MigrationExecutor(
connection, self.TEST_MIGRATIONS_DIR)
connection, self.TEST_MIGRATIONS_DIR)

first = migration.Migration('1', None)
second = migration.Migration('2', '1')
third = migration.Migration('3', '2')
first = migration.Migration('1', None, '1')
second = migration.Migration('2', '1', '2')
third = migration.Migration('3', '2', '3')
with mock.patch.object(executor, 'migrations') as migrations:
migrations.return_value = [first, second, third]
migrated = {'1': True, '2': False, '3': False}
with mock.patch.object(executor, '_migration_status_map', migrated):
executor.migrate()
self.assertEqual(migrated, {'1': True, '2': True, '3': True})

def test_show_migrations(self):
connection = mock.Mock()
executor = migration_executor.MigrationExecutor(
connection, self.TEST_MIGRATIONS_DIR)

first = migration.Migration('abcdef', None, '1')
second = migration.Migration('012345', 'abcdef', '2')
third = migration.Migration('6abcde', '012345', '3')
with mock.patch.object(executor, 'migrations') as migrations:
migrations.return_value = [first, second, third]
migrated = {'abcdef': True, '012345': False, '6abcde': False}
with mock.patch.object(executor, '_migration_status_map', migrated):
with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:
executor.show_migrations()
self.assertEqual("[ ] 6abcde, 3\n[ ] 012345, 2\n[X] abcdef, 1\n", mock_stdout.getvalue())

def test_rollback(self):
connection = mock.Mock()
executor = migration_executor.MigrationExecutor(
connection, self.TEST_MIGRATIONS_DIR)

first = migration.Migration('1', None)
second = migration.Migration('2', '1')
third = migration.Migration('3', '2')
first = migration.Migration('1', None, '1')
second = migration.Migration('2', '1', '2')
third = migration.Migration('3', '2', '3')
with mock.patch.object(executor, 'migrations') as migrations:
migrations.return_value = [first, second, third]
migrated = {'1': True, '2': False, '3': False}
Expand Down