Skip to content

Commit 7580472

Browse files
committed
Add support for schema= in migration tooling.
1 parent b305882 commit 7580472

4 files changed

Lines changed: 95 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ Improvements:
3333
`get_primary_keys()`, `get_foreign_keys()`, `get_views()`) with no `schema`
3434
now follows the search path via `current_schema()` instead of assuming
3535
`public`, matching MySQL's `DATABASE()` behavior.
36+
* Add `Runner(db, schema=...)` and `pwmigrate --schema` for running
37+
migrations against a specific schema. The history table lives in that
38+
schema, so each schema tracks its own applied set and one set of migration
39+
files can be run against any number of schemas.
3640
* Add `SchemaMigrator(db, schema=...)`, which qualifies every table name w/the
3741
given schema (or database on MySQL). Not supported for SQLite, as the
3842
table-rebuild rewrites DDL straight from `sqlite_master`.

docs/peewee/db_tools.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,7 @@ Recognized keys:
10101010
* ``database`` - database spec (dotted path, url, or sqlite filename)
10111011
* ``directory`` - migrations directory
10121012
* ``models`` - models module, read by ``diff``, ``initial`` and ``generate``
1013+
* ``schema`` - schema containing the tables to be migrated
10131014
* ``table`` - history table name
10141015

10151016
The file is read from the working directory only. A different config
@@ -1111,7 +1112,17 @@ Generate a migration from a diff:
11111112
if diff:
11121113
runner.create('add karma', body=template(diff))
11131114
1114-
.. class:: Runner(database, directory='migrations', table_name='schema_migration')
1115+
.. class:: Runner(database, directory='migrations', table_name='schema_migration', schema=None)
1116+
1117+
:param str schema: schema containing the tables to be migrated, passed to
1118+
the :class:`SchemaMigrator`. The history table lives in the same
1119+
schema, so each schema tracks its own applied set and one set of
1120+
migration files can be run against any number of schemas
1121+
(``pwmigrate up -s tenant_a``, ``pwmigrate up -s tenant_b``).
1122+
1123+
When adopting ``schema=`` on a deployment whose history predates it,
1124+
the runner will find no history there and consider every migration
1125+
pending. Backfill with ``fake`` first.
11151126

11161127
.. method:: up(target=None)
11171128

playhouse/migrations.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,22 @@ def load(self):
8989

9090
class Runner(object):
9191
def __init__(self, database, directory='migrations',
92-
table_name='schema_migration'):
92+
table_name='schema_migration', schema=None):
9393
self.database = database
9494
self.directory = directory
9595
self.table_name = table_name
96-
self.migrator = SchemaMigrator.from_database(database)
96+
self.schema = schema
97+
self.migrator = SchemaMigrator.from_database(database, schema=schema)
9798

99+
# History lives in the schema it describes, so each schema tracks
100+
# its own applied set.
98101
class History(database.Model):
99102
name = CharField(unique=True)
100103
applied = DateTimeField(default=datetime.datetime.now)
101104
class Meta:
102105
legacy_table_names = False
103106
table_name = self.table_name
107+
schema = self.schema
104108

105109
self.History = History
106110

@@ -496,7 +500,7 @@ def template(diff):
496500
for f in diff.add_columns if f.model._meta.schema)
497501
if qualified:
498502
todos.append('schema-qualified tables (%s): column/index '
499-
'operations are emitted unqualified. Qualify by hand' %
503+
'operations are emitted unqualified. Run with --schema' %
500504
', '.join(sorted(qualified)))
501505

502506
up, down = [], []
@@ -713,7 +717,7 @@ def _read_config(path):
713717
key, _, value = line.partition('=')
714718
config[key.strip()] = value.strip()
715719
for key in sorted(set(config) - {'database', 'directory', 'models',
716-
'table'}):
720+
'schema', 'table'}):
717721
sys.stderr.write('warning: unknown key "%s" in %s\n' % (key, path))
718722
return config
719723

@@ -735,6 +739,9 @@ def _parser(config):
735739
common.add_argument('-d', '--directory',
736740
default=config.get('directory', 'migrations'),
737741
help='migrations directory (default: migrations)')
742+
common.add_argument('-s', '--schema',
743+
default=config.get('schema'),
744+
help='schema containing the tables to be migrated')
738745
common.add_argument('-t', '--table',
739746
default=config.get('table', 'schema_migration'),
740747
help='history table name')
@@ -823,10 +830,10 @@ def main(argv=None):
823830
database = None
824831
try:
825832
database = _resolve_database(args.database)
826-
runner = Runner(database, args.directory, args.table)
833+
runner = Runner(database, args.directory, args.table, args.schema)
827834
return args.func(runner, args) or 0
828835
except (MigrationError, DatabaseError, InterfaceError,
829-
ImproperlyConfigured) as exc:
836+
ImproperlyConfigured, ValueError) as exc:
830837
if args.verbose:
831838
traceback.print_exc()
832839
sys.stderr.write('error: %s\n' % exc)

tests/migrations.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,72 @@ def run_cli(*args):
17311731
return rc, out.getvalue(), err.getvalue()
17321732

17331733

1734+
@requires_postgresql
1735+
class TestRunnerSchema(DatabaseTestCase):
1736+
"""One set of migration files runs against any number of schemas."""
1737+
target = 'runner_target'
1738+
decoy = 'runner_decoy'
1739+
1740+
def setUp(self):
1741+
super(TestRunnerSchema, self).setUp()
1742+
self.dir = tempfile.mkdtemp()
1743+
for schema in (self.target, self.decoy):
1744+
self.execute('DROP SCHEMA IF EXISTS %s CASCADE' % schema)
1745+
self.execute('CREATE SCHEMA %s' % schema)
1746+
self.execute('CREATE TABLE %s.person (id SERIAL PRIMARY KEY, '
1747+
'first_name TEXT)' % schema)
1748+
with open(os.path.join(self.dir, '0001_notes.py'), 'w') as fh:
1749+
fh.write(add_column_mig('notes'))
1750+
self.runner = Runner(self.database, self.dir, schema=self.target)
1751+
1752+
def tearDown(self):
1753+
try:
1754+
shutil.rmtree(self.dir, ignore_errors=True)
1755+
for schema in (self.target, self.decoy):
1756+
self.execute('DROP SCHEMA IF EXISTS %s CASCADE' % schema)
1757+
finally:
1758+
super(TestRunnerSchema, self).tearDown()
1759+
1760+
def columns(self, schema):
1761+
return sorted(c.name for c in
1762+
self.database.get_columns('person', schema))
1763+
1764+
def tables(self, schema):
1765+
return self.database.get_tables(schema)
1766+
1767+
def test_up_down(self):
1768+
self.assertEqual(self.runner.up(), ['0001_notes'])
1769+
self.assertEqual(self.columns(self.target),
1770+
['first_name', 'id', 'notes'])
1771+
self.assertEqual(self.columns(self.decoy), ['first_name', 'id'])
1772+
# History lives in the schema it describes.
1773+
self.assertTrue('schema_migration' in self.tables(self.target))
1774+
self.assertFalse('schema_migration' in self.tables(self.decoy))
1775+
1776+
self.assertEqual(self.runner.down(), ['0001_notes'])
1777+
self.assertEqual(self.columns(self.target), ['first_name', 'id'])
1778+
1779+
def test_per_schema_history(self):
1780+
self.runner.up()
1781+
other = Runner(self.database, self.dir, schema=self.decoy)
1782+
self.assertEqual(other.up(), ['0001_notes'])
1783+
self.assertEqual(self.columns(self.decoy),
1784+
['first_name', 'id', 'notes'])
1785+
self.assertEqual(sorted(self.runner.applied()), ['0001_notes'])
1786+
self.assertEqual(sorted(other.applied()), ['0001_notes'])
1787+
1788+
# Each schema reverts independently.
1789+
other.down()
1790+
self.assertEqual(self.columns(self.decoy), ['first_name', 'id'])
1791+
self.assertEqual(self.columns(self.target),
1792+
['first_name', 'id', 'notes'])
1793+
1794+
def test_fake_backfill(self):
1795+
self.assertEqual(self.runner.fake(), ['0001_notes'])
1796+
self.assertEqual(self.columns(self.target), ['first_name', 'id'])
1797+
self.assertEqual(self.runner.up(), [])
1798+
1799+
17341800
class TestMigrationRunnerCLI(BaseTestCase):
17351801
def setUp(self):
17361802
super(TestMigrationRunnerCLI, self).setUp()

0 commit comments

Comments
 (0)