Show More
@@ -1,220 +1,221 | |||||
1 | """ |
|
1 | """ | |
2 | Database schema version management. |
|
2 | Database schema version management. | |
3 | """ |
|
3 | """ | |
4 | import sys |
|
4 | import sys | |
5 | import logging |
|
5 | import logging | |
6 |
|
6 | |||
7 | from sqlalchemy import (Table, Column, MetaData, String, Text, Integer, |
|
7 | from sqlalchemy import (Table, Column, MetaData, String, Text, Integer, | |
8 | create_engine) |
|
8 | create_engine) | |
9 | from sqlalchemy.sql import and_ |
|
9 | from sqlalchemy.sql import and_ | |
10 |
from sqlalchemy import exc |
|
10 | from sqlalchemy import exc as sa_exceptions | |
11 | from sqlalchemy.sql import bindparam |
|
11 | from sqlalchemy.sql import bindparam | |
12 |
|
12 | |||
13 | from rhodecode.lib.dbmigrate.migrate import exceptions |
|
13 | from rhodecode.lib.dbmigrate.migrate import exceptions | |
14 | from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07 |
|
14 | from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07 | |
15 | from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff |
|
15 | from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff | |
16 | from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository |
|
16 | from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository | |
17 | from rhodecode.lib.dbmigrate.migrate.versioning.util import load_model |
|
17 | from rhodecode.lib.dbmigrate.migrate.versioning.util import load_model | |
18 | from rhodecode.lib.dbmigrate.migrate.versioning.version import VerNum |
|
18 | from rhodecode.lib.dbmigrate.migrate.versioning.version import VerNum | |
19 |
|
19 | |||
20 |
|
20 | |||
21 | log = logging.getLogger(__name__) |
|
21 | log = logging.getLogger(__name__) | |
22 |
|
22 | |||
|
23 | ||||
23 | class ControlledSchema(object): |
|
24 | class ControlledSchema(object): | |
24 | """A database under version control""" |
|
25 | """A database under version control""" | |
25 |
|
26 | |||
26 | def __init__(self, engine, repository): |
|
27 | def __init__(self, engine, repository): | |
27 | if isinstance(repository, basestring): |
|
28 | if isinstance(repository, basestring): | |
28 | repository = Repository(repository) |
|
29 | repository = Repository(repository) | |
29 | self.engine = engine |
|
30 | self.engine = engine | |
30 | self.repository = repository |
|
31 | self.repository = repository | |
31 | self.meta = MetaData(engine) |
|
32 | self.meta = MetaData(engine) | |
32 | self.load() |
|
33 | self.load() | |
33 |
|
34 | |||
34 | def __eq__(self, other): |
|
35 | def __eq__(self, other): | |
35 | """Compare two schemas by repositories and versions""" |
|
36 | """Compare two schemas by repositories and versions""" | |
36 | return (self.repository is other.repository \ |
|
37 | return (self.repository is other.repository \ | |
37 | and self.version == other.version) |
|
38 | and self.version == other.version) | |
38 |
|
39 | |||
39 | def load(self): |
|
40 | def load(self): | |
40 | """Load controlled schema version info from DB""" |
|
41 | """Load controlled schema version info from DB""" | |
41 | tname = self.repository.version_table |
|
42 | tname = self.repository.version_table | |
42 | try: |
|
43 | try: | |
43 | if not hasattr(self, 'table') or self.table is None: |
|
44 | if not hasattr(self, 'table') or self.table is None: | |
44 | self.table = Table(tname, self.meta, autoload=True) |
|
45 | self.table = Table(tname, self.meta, autoload=True) | |
45 |
|
46 | |||
46 | result = self.engine.execute(self.table.select( |
|
47 | result = self.engine.execute(self.table.select( | |
47 | self.table.c.repository_id == str(self.repository.id))) |
|
48 | self.table.c.repository_id == str(self.repository.id))) | |
48 |
|
49 | |||
49 | data = list(result)[0] |
|
50 | data = list(result)[0] | |
50 | except: |
|
51 | except: | |
51 | cls, exc, tb = sys.exc_info() |
|
52 | cls, exc, tb = sys.exc_info() | |
52 | raise exceptions.DatabaseNotControlledError, exc.__str__(), tb |
|
53 | raise exceptions.DatabaseNotControlledError, exc.__str__(), tb | |
53 |
|
54 | |||
54 | self.version = data['version'] |
|
55 | self.version = data['version'] | |
55 | return data |
|
56 | return data | |
56 |
|
57 | |||
57 | def drop(self): |
|
58 | def drop(self): | |
58 | """ |
|
59 | """ | |
59 | Remove version control from a database. |
|
60 | Remove version control from a database. | |
60 | """ |
|
61 | """ | |
61 | if SQLA_07: |
|
62 | if SQLA_07: | |
62 | try: |
|
63 | try: | |
63 | self.table.drop() |
|
64 | self.table.drop() | |
64 | except sa_exceptions.DatabaseError: |
|
65 | except sa_exceptions.DatabaseError: | |
65 | raise exceptions.DatabaseNotControlledError(str(self.table)) |
|
66 | raise exceptions.DatabaseNotControlledError(str(self.table)) | |
66 | else: |
|
67 | else: | |
67 | try: |
|
68 | try: | |
68 | self.table.drop() |
|
69 | self.table.drop() | |
69 | except (sa_exceptions.SQLError): |
|
70 | except (sa_exceptions.SQLError): | |
70 | raise exceptions.DatabaseNotControlledError(str(self.table)) |
|
71 | raise exceptions.DatabaseNotControlledError(str(self.table)) | |
71 |
|
72 | |||
72 | def changeset(self, version=None): |
|
73 | def changeset(self, version=None): | |
73 | """API to Changeset creation. |
|
74 | """API to Changeset creation. | |
74 |
|
75 | |||
75 | Uses self.version for start version and engine.name |
|
76 | Uses self.version for start version and engine.name | |
76 | to get database name. |
|
77 | to get database name. | |
77 | """ |
|
78 | """ | |
78 | database = self.engine.name |
|
79 | database = self.engine.name | |
79 | start_ver = self.version |
|
80 | start_ver = self.version | |
80 | changeset = self.repository.changeset(database, start_ver, version) |
|
81 | changeset = self.repository.changeset(database, start_ver, version) | |
81 | return changeset |
|
82 | return changeset | |
82 |
|
83 | |||
83 | def runchange(self, ver, change, step): |
|
84 | def runchange(self, ver, change, step): | |
84 | startver = ver |
|
85 | startver = ver | |
85 | endver = ver + step |
|
86 | endver = ver + step | |
86 | # Current database version must be correct! Don't run if corrupt! |
|
87 | # Current database version must be correct! Don't run if corrupt! | |
87 | if self.version != startver: |
|
88 | if self.version != startver: | |
88 | raise exceptions.InvalidVersionError("%s is not %s" % \ |
|
89 | raise exceptions.InvalidVersionError("%s is not %s" % \ | |
89 | (self.version, startver)) |
|
90 | (self.version, startver)) | |
90 | # Run the change |
|
91 | # Run the change | |
91 | change.run(self.engine, step) |
|
92 | change.run(self.engine, step) | |
92 |
|
93 | |||
93 | # Update/refresh database version |
|
94 | # Update/refresh database version | |
94 | self.update_repository_table(startver, endver) |
|
95 | self.update_repository_table(startver, endver) | |
95 | self.load() |
|
96 | self.load() | |
96 |
|
97 | |||
97 | def update_repository_table(self, startver, endver): |
|
98 | def update_repository_table(self, startver, endver): | |
98 | """Update version_table with new information""" |
|
99 | """Update version_table with new information""" | |
99 | update = self.table.update(and_(self.table.c.version == int(startver), |
|
100 | update = self.table.update(and_(self.table.c.version == int(startver), | |
100 | self.table.c.repository_id == str(self.repository.id))) |
|
101 | self.table.c.repository_id == str(self.repository.id))) | |
101 | self.engine.execute(update, version=int(endver)) |
|
102 | self.engine.execute(update, version=int(endver)) | |
102 |
|
103 | |||
103 | def upgrade(self, version=None): |
|
104 | def upgrade(self, version=None): | |
104 | """ |
|
105 | """ | |
105 | Upgrade (or downgrade) to a specified version, or latest version. |
|
106 | Upgrade (or downgrade) to a specified version, or latest version. | |
106 | """ |
|
107 | """ | |
107 | changeset = self.changeset(version) |
|
108 | changeset = self.changeset(version) | |
108 | for ver, change in changeset: |
|
109 | for ver, change in changeset: | |
109 | self.runchange(ver, change, changeset.step) |
|
110 | self.runchange(ver, change, changeset.step) | |
110 |
|
111 | |||
111 | def update_db_from_model(self, model): |
|
112 | def update_db_from_model(self, model): | |
112 | """ |
|
113 | """ | |
113 | Modify the database to match the structure of the current Python model. |
|
114 | Modify the database to match the structure of the current Python model. | |
114 | """ |
|
115 | """ | |
115 | model = load_model(model) |
|
116 | model = load_model(model) | |
116 |
|
117 | |||
117 | diff = schemadiff.getDiffOfModelAgainstDatabase( |
|
118 | diff = schemadiff.getDiffOfModelAgainstDatabase( | |
118 | model, self.engine, excludeTables=[self.repository.version_table] |
|
119 | model, self.engine, excludeTables=[self.repository.version_table] | |
119 | ) |
|
120 | ) | |
120 | genmodel.ModelGenerator(diff,self.engine).runB2A() |
|
121 | genmodel.ModelGenerator(diff,self.engine).runB2A() | |
121 |
|
122 | |||
122 | self.update_repository_table(self.version, int(self.repository.latest)) |
|
123 | self.update_repository_table(self.version, int(self.repository.latest)) | |
123 |
|
124 | |||
124 | self.load() |
|
125 | self.load() | |
125 |
|
126 | |||
126 | @classmethod |
|
127 | @classmethod | |
127 | def create(cls, engine, repository, version=None): |
|
128 | def create(cls, engine, repository, version=None): | |
128 | """ |
|
129 | """ | |
129 | Declare a database to be under a repository's version control. |
|
130 | Declare a database to be under a repository's version control. | |
130 |
|
131 | |||
131 | :raises: :exc:`DatabaseAlreadyControlledError` |
|
132 | :raises: :exc:`DatabaseAlreadyControlledError` | |
132 | :returns: :class:`ControlledSchema` |
|
133 | :returns: :class:`ControlledSchema` | |
133 | """ |
|
134 | """ | |
134 | # Confirm that the version # is valid: positive, integer, |
|
135 | # Confirm that the version # is valid: positive, integer, | |
135 | # exists in repos |
|
136 | # exists in repos | |
136 | if isinstance(repository, basestring): |
|
137 | if isinstance(repository, basestring): | |
137 | repository = Repository(repository) |
|
138 | repository = Repository(repository) | |
138 | version = cls._validate_version(repository, version) |
|
139 | version = cls._validate_version(repository, version) | |
139 | table = cls._create_table_version(engine, repository, version) |
|
140 | table = cls._create_table_version(engine, repository, version) | |
140 | # TODO: history table |
|
141 | # TODO: history table | |
141 | # Load repository information and return |
|
142 | # Load repository information and return | |
142 | return cls(engine, repository) |
|
143 | return cls(engine, repository) | |
143 |
|
144 | |||
144 | @classmethod |
|
145 | @classmethod | |
145 | def _validate_version(cls, repository, version): |
|
146 | def _validate_version(cls, repository, version): | |
146 | """ |
|
147 | """ | |
147 | Ensures this is a valid version number for this repository. |
|
148 | Ensures this is a valid version number for this repository. | |
148 |
|
149 | |||
149 | :raises: :exc:`InvalidVersionError` if invalid |
|
150 | :raises: :exc:`InvalidVersionError` if invalid | |
150 | :return: valid version number |
|
151 | :return: valid version number | |
151 | """ |
|
152 | """ | |
152 | if version is None: |
|
153 | if version is None: | |
153 | version = 0 |
|
154 | version = 0 | |
154 | try: |
|
155 | try: | |
155 | version = VerNum(version) # raises valueerror |
|
156 | version = VerNum(version) # raises valueerror | |
156 | if version < 0 or version > repository.latest: |
|
157 | if version < 0 or version > repository.latest: | |
157 | raise ValueError() |
|
158 | raise ValueError() | |
158 | except ValueError: |
|
159 | except ValueError: | |
159 | raise exceptions.InvalidVersionError(version) |
|
160 | raise exceptions.InvalidVersionError(version) | |
160 | return version |
|
161 | return version | |
161 |
|
162 | |||
162 | @classmethod |
|
163 | @classmethod | |
163 | def _create_table_version(cls, engine, repository, version): |
|
164 | def _create_table_version(cls, engine, repository, version): | |
164 | """ |
|
165 | """ | |
165 | Creates the versioning table in a database. |
|
166 | Creates the versioning table in a database. | |
166 |
|
167 | |||
167 | :raises: :exc:`DatabaseAlreadyControlledError` |
|
168 | :raises: :exc:`DatabaseAlreadyControlledError` | |
168 | """ |
|
169 | """ | |
169 | # Create tables |
|
170 | # Create tables | |
170 | tname = repository.version_table |
|
171 | tname = repository.version_table | |
171 | meta = MetaData(engine) |
|
172 | meta = MetaData(engine) | |
172 |
|
173 | |||
173 | table = Table( |
|
174 | table = Table( | |
174 | tname, meta, |
|
175 | tname, meta, | |
175 | Column('repository_id', String(250), primary_key=True), |
|
176 | Column('repository_id', String(250), primary_key=True), | |
176 | Column('repository_path', Text), |
|
177 | Column('repository_path', Text), | |
177 | Column('version', Integer), ) |
|
178 | Column('version', Integer), ) | |
178 |
|
179 | |||
179 | # there can be multiple repositories/schemas in the same db |
|
180 | # there can be multiple repositories/schemas in the same db | |
180 | if not table.exists(): |
|
181 | if not table.exists(): | |
181 | table.create() |
|
182 | table.create() | |
182 |
|
183 | |||
183 | # test for existing repository_id |
|
184 | # test for existing repository_id | |
184 | s = table.select(table.c.repository_id == bindparam("repository_id")) |
|
185 | s = table.select(table.c.repository_id == bindparam("repository_id")) | |
185 | result = engine.execute(s, repository_id=repository.id) |
|
186 | result = engine.execute(s, repository_id=repository.id) | |
186 | if result.fetchone(): |
|
187 | if result.fetchone(): | |
187 | raise exceptions.DatabaseAlreadyControlledError |
|
188 | raise exceptions.DatabaseAlreadyControlledError | |
188 |
|
189 | |||
189 | # Insert data |
|
190 | # Insert data | |
190 | engine.execute(table.insert().values( |
|
191 | engine.execute(table.insert().values( | |
191 | repository_id=repository.id, |
|
192 | repository_id=repository.id, | |
192 | repository_path=repository.path, |
|
193 | repository_path=repository.path, | |
193 | version=int(version))) |
|
194 | version=int(version))) | |
194 | return table |
|
195 | return table | |
195 |
|
196 | |||
196 | @classmethod |
|
197 | @classmethod | |
197 | def compare_model_to_db(cls, engine, model, repository): |
|
198 | def compare_model_to_db(cls, engine, model, repository): | |
198 | """ |
|
199 | """ | |
199 | Compare the current model against the current database. |
|
200 | Compare the current model against the current database. | |
200 | """ |
|
201 | """ | |
201 | if isinstance(repository, basestring): |
|
202 | if isinstance(repository, basestring): | |
202 | repository = Repository(repository) |
|
203 | repository = Repository(repository) | |
203 | model = load_model(model) |
|
204 | model = load_model(model) | |
204 |
|
205 | |||
205 | diff = schemadiff.getDiffOfModelAgainstDatabase( |
|
206 | diff = schemadiff.getDiffOfModelAgainstDatabase( | |
206 | model, engine, excludeTables=[repository.version_table]) |
|
207 | model, engine, excludeTables=[repository.version_table]) | |
207 | return diff |
|
208 | return diff | |
208 |
|
209 | |||
209 | @classmethod |
|
210 | @classmethod | |
210 | def create_model(cls, engine, repository, declarative=False): |
|
211 | def create_model(cls, engine, repository, declarative=False): | |
211 | """ |
|
212 | """ | |
212 | Dump the current database as a Python model. |
|
213 | Dump the current database as a Python model. | |
213 | """ |
|
214 | """ | |
214 | if isinstance(repository, basestring): |
|
215 | if isinstance(repository, basestring): | |
215 | repository = Repository(repository) |
|
216 | repository = Repository(repository) | |
216 |
|
217 | |||
217 | diff = schemadiff.getDiffOfModelAgainstDatabase( |
|
218 | diff = schemadiff.getDiffOfModelAgainstDatabase( | |
218 | MetaData(), engine, excludeTables=[repository.version_table] |
|
219 | MetaData(), engine, excludeTables=[repository.version_table] | |
219 | ) |
|
220 | ) | |
220 | return genmodel.ModelGenerator(diff, engine, declarative).genBDefinition() |
|
221 | return genmodel.ModelGenerator(diff, engine, declarative).genBDefinition() |
@@ -1,293 +1,295 | |||||
1 | """ |
|
1 | """ | |
2 | Schema differencing support. |
|
2 | Schema differencing support. | |
3 | """ |
|
3 | """ | |
4 |
|
4 | |||
5 | import logging |
|
5 | import logging | |
6 | import sqlalchemy |
|
6 | import sqlalchemy | |
7 |
|
7 | |||
8 | from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06 |
|
8 | from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06 | |
9 | from sqlalchemy.types import Float |
|
9 | from sqlalchemy.types import Float | |
10 |
|
10 | |||
11 | log = logging.getLogger(__name__) |
|
11 | log = logging.getLogger(__name__) | |
12 |
|
12 | |||
|
13 | ||||
13 | def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None): |
|
14 | def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None): | |
14 | """ |
|
15 | """ | |
15 | Return differences of model against database. |
|
16 | Return differences of model against database. | |
16 |
|
17 | |||
17 | :return: object which will evaluate to :keyword:`True` if there \ |
|
18 | :return: object which will evaluate to :keyword:`True` if there \ | |
18 | are differences else :keyword:`False`. |
|
19 | are differences else :keyword:`False`. | |
19 | """ |
|
20 | """ | |
20 |
db_metadata = sqlalchemy.MetaData(engine |
|
21 | db_metadata = sqlalchemy.MetaData(engine) | |
|
22 | db_metadata.reflect() | |||
21 |
|
23 | |||
22 | # sqlite will include a dynamically generated 'sqlite_sequence' table if |
|
24 | # sqlite will include a dynamically generated 'sqlite_sequence' table if | |
23 | # there are autoincrement sequences in the database; this should not be |
|
25 | # there are autoincrement sequences in the database; this should not be | |
24 | # compared. |
|
26 | # compared. | |
25 | if engine.dialect.name == 'sqlite': |
|
27 | if engine.dialect.name == 'sqlite': | |
26 | if 'sqlite_sequence' in db_metadata.tables: |
|
28 | if 'sqlite_sequence' in db_metadata.tables: | |
27 | db_metadata.remove(db_metadata.tables['sqlite_sequence']) |
|
29 | db_metadata.remove(db_metadata.tables['sqlite_sequence']) | |
28 |
|
30 | |||
29 | return SchemaDiff(metadata, db_metadata, |
|
31 | return SchemaDiff(metadata, db_metadata, | |
30 | labelA='model', |
|
32 | labelA='model', | |
31 | labelB='database', |
|
33 | labelB='database', | |
32 | excludeTables=excludeTables) |
|
34 | excludeTables=excludeTables) | |
33 |
|
35 | |||
34 |
|
36 | |||
35 | def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None): |
|
37 | def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None): | |
36 | """ |
|
38 | """ | |
37 | Return differences of model against another model. |
|
39 | Return differences of model against another model. | |
38 |
|
40 | |||
39 | :return: object which will evaluate to :keyword:`True` if there \ |
|
41 | :return: object which will evaluate to :keyword:`True` if there \ | |
40 | are differences else :keyword:`False`. |
|
42 | are differences else :keyword:`False`. | |
41 | """ |
|
43 | """ | |
42 | return SchemaDiff(metadataA, metadataB, excludeTables=excludeTables) |
|
44 | return SchemaDiff(metadataA, metadataB, excludeTables=excludeTables) | |
43 |
|
45 | |||
44 |
|
46 | |||
45 | class ColDiff(object): |
|
47 | class ColDiff(object): | |
46 | """ |
|
48 | """ | |
47 | Container for differences in one :class:`~sqlalchemy.schema.Column` |
|
49 | Container for differences in one :class:`~sqlalchemy.schema.Column` | |
48 | between two :class:`~sqlalchemy.schema.Table` instances, ``A`` |
|
50 | between two :class:`~sqlalchemy.schema.Table` instances, ``A`` | |
49 | and ``B``. |
|
51 | and ``B``. | |
50 |
|
52 | |||
51 | .. attribute:: col_A |
|
53 | .. attribute:: col_A | |
52 |
|
54 | |||
53 | The :class:`~sqlalchemy.schema.Column` object for A. |
|
55 | The :class:`~sqlalchemy.schema.Column` object for A. | |
54 |
|
56 | |||
55 | .. attribute:: col_B |
|
57 | .. attribute:: col_B | |
56 |
|
58 | |||
57 | The :class:`~sqlalchemy.schema.Column` object for B. |
|
59 | The :class:`~sqlalchemy.schema.Column` object for B. | |
58 |
|
60 | |||
59 | .. attribute:: type_A |
|
61 | .. attribute:: type_A | |
60 |
|
62 | |||
61 | The most generic type of the :class:`~sqlalchemy.schema.Column` |
|
63 | The most generic type of the :class:`~sqlalchemy.schema.Column` | |
62 | object in A. |
|
64 | object in A. | |
63 |
|
65 | |||
64 | .. attribute:: type_B |
|
66 | .. attribute:: type_B | |
65 |
|
67 | |||
66 | The most generic type of the :class:`~sqlalchemy.schema.Column` |
|
68 | The most generic type of the :class:`~sqlalchemy.schema.Column` | |
67 | object in A. |
|
69 | object in A. | |
68 |
|
70 | |||
69 | """ |
|
71 | """ | |
70 |
|
72 | |||
71 | diff = False |
|
73 | diff = False | |
72 |
|
74 | |||
73 | def __init__(self,col_A,col_B): |
|
75 | def __init__(self,col_A,col_B): | |
74 | self.col_A = col_A |
|
76 | self.col_A = col_A | |
75 | self.col_B = col_B |
|
77 | self.col_B = col_B | |
76 |
|
78 | |||
77 | self.type_A = col_A.type |
|
79 | self.type_A = col_A.type | |
78 | self.type_B = col_B.type |
|
80 | self.type_B = col_B.type | |
79 |
|
81 | |||
80 | self.affinity_A = self.type_A._type_affinity |
|
82 | self.affinity_A = self.type_A._type_affinity | |
81 | self.affinity_B = self.type_B._type_affinity |
|
83 | self.affinity_B = self.type_B._type_affinity | |
82 |
|
84 | |||
83 | if self.affinity_A is not self.affinity_B: |
|
85 | if self.affinity_A is not self.affinity_B: | |
84 | self.diff = True |
|
86 | self.diff = True | |
85 | return |
|
87 | return | |
86 |
|
88 | |||
87 | if isinstance(self.type_A,Float) or isinstance(self.type_B,Float): |
|
89 | if isinstance(self.type_A,Float) or isinstance(self.type_B,Float): | |
88 | if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)): |
|
90 | if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)): | |
89 | self.diff=True |
|
91 | self.diff=True | |
90 | return |
|
92 | return | |
91 |
|
93 | |||
92 | for attr in ('precision','scale','length'): |
|
94 | for attr in ('precision','scale','length'): | |
93 | A = getattr(self.type_A,attr,None) |
|
95 | A = getattr(self.type_A,attr,None) | |
94 | B = getattr(self.type_B,attr,None) |
|
96 | B = getattr(self.type_B,attr,None) | |
95 | if not (A is None or B is None) and A!=B: |
|
97 | if not (A is None or B is None) and A!=B: | |
96 | self.diff=True |
|
98 | self.diff=True | |
97 | return |
|
99 | return | |
98 |
|
100 | |||
99 | def __nonzero__(self): |
|
101 | def __nonzero__(self): | |
100 | return self.diff |
|
102 | return self.diff | |
101 |
|
103 | |||
102 | class TableDiff(object): |
|
104 | class TableDiff(object): | |
103 | """ |
|
105 | """ | |
104 | Container for differences in one :class:`~sqlalchemy.schema.Table` |
|
106 | Container for differences in one :class:`~sqlalchemy.schema.Table` | |
105 | between two :class:`~sqlalchemy.schema.MetaData` instances, ``A`` |
|
107 | between two :class:`~sqlalchemy.schema.MetaData` instances, ``A`` | |
106 | and ``B``. |
|
108 | and ``B``. | |
107 |
|
109 | |||
108 | .. attribute:: columns_missing_from_A |
|
110 | .. attribute:: columns_missing_from_A | |
109 |
|
111 | |||
110 | A sequence of column names that were found in B but weren't in |
|
112 | A sequence of column names that were found in B but weren't in | |
111 | A. |
|
113 | A. | |
112 |
|
114 | |||
113 | .. attribute:: columns_missing_from_B |
|
115 | .. attribute:: columns_missing_from_B | |
114 |
|
116 | |||
115 | A sequence of column names that were found in A but weren't in |
|
117 | A sequence of column names that were found in A but weren't in | |
116 | B. |
|
118 | B. | |
117 |
|
119 | |||
118 | .. attribute:: columns_different |
|
120 | .. attribute:: columns_different | |
119 |
|
121 | |||
120 | A dictionary containing information about columns that were |
|
122 | A dictionary containing information about columns that were | |
121 | found to be different. |
|
123 | found to be different. | |
122 | It maps column names to a :class:`ColDiff` objects describing the |
|
124 | It maps column names to a :class:`ColDiff` objects describing the | |
123 | differences found. |
|
125 | differences found. | |
124 | """ |
|
126 | """ | |
125 | __slots__ = ( |
|
127 | __slots__ = ( | |
126 | 'columns_missing_from_A', |
|
128 | 'columns_missing_from_A', | |
127 | 'columns_missing_from_B', |
|
129 | 'columns_missing_from_B', | |
128 | 'columns_different', |
|
130 | 'columns_different', | |
129 | ) |
|
131 | ) | |
130 |
|
132 | |||
131 | def __nonzero__(self): |
|
133 | def __nonzero__(self): | |
132 | return bool( |
|
134 | return bool( | |
133 | self.columns_missing_from_A or |
|
135 | self.columns_missing_from_A or | |
134 | self.columns_missing_from_B or |
|
136 | self.columns_missing_from_B or | |
135 | self.columns_different |
|
137 | self.columns_different | |
136 | ) |
|
138 | ) | |
137 |
|
139 | |||
138 | class SchemaDiff(object): |
|
140 | class SchemaDiff(object): | |
139 | """ |
|
141 | """ | |
140 | Compute the difference between two :class:`~sqlalchemy.schema.MetaData` |
|
142 | Compute the difference between two :class:`~sqlalchemy.schema.MetaData` | |
141 | objects. |
|
143 | objects. | |
142 |
|
144 | |||
143 | The string representation of a :class:`SchemaDiff` will summarise |
|
145 | The string representation of a :class:`SchemaDiff` will summarise | |
144 | the changes found between the two |
|
146 | the changes found between the two | |
145 | :class:`~sqlalchemy.schema.MetaData` objects. |
|
147 | :class:`~sqlalchemy.schema.MetaData` objects. | |
146 |
|
148 | |||
147 | The length of a :class:`SchemaDiff` will give the number of |
|
149 | The length of a :class:`SchemaDiff` will give the number of | |
148 | changes found, enabling it to be used much like a boolean in |
|
150 | changes found, enabling it to be used much like a boolean in | |
149 | expressions. |
|
151 | expressions. | |
150 |
|
152 | |||
151 | :param metadataA: |
|
153 | :param metadataA: | |
152 | First :class:`~sqlalchemy.schema.MetaData` to compare. |
|
154 | First :class:`~sqlalchemy.schema.MetaData` to compare. | |
153 |
|
155 | |||
154 | :param metadataB: |
|
156 | :param metadataB: | |
155 | Second :class:`~sqlalchemy.schema.MetaData` to compare. |
|
157 | Second :class:`~sqlalchemy.schema.MetaData` to compare. | |
156 |
|
158 | |||
157 | :param labelA: |
|
159 | :param labelA: | |
158 | The label to use in messages about the first |
|
160 | The label to use in messages about the first | |
159 | :class:`~sqlalchemy.schema.MetaData`. |
|
161 | :class:`~sqlalchemy.schema.MetaData`. | |
160 |
|
162 | |||
161 | :param labelB: |
|
163 | :param labelB: | |
162 | The label to use in messages about the second |
|
164 | The label to use in messages about the second | |
163 | :class:`~sqlalchemy.schema.MetaData`. |
|
165 | :class:`~sqlalchemy.schema.MetaData`. | |
164 |
|
166 | |||
165 | :param excludeTables: |
|
167 | :param excludeTables: | |
166 | A sequence of table names to exclude. |
|
168 | A sequence of table names to exclude. | |
167 |
|
169 | |||
168 | .. attribute:: tables_missing_from_A |
|
170 | .. attribute:: tables_missing_from_A | |
169 |
|
171 | |||
170 | A sequence of table names that were found in B but weren't in |
|
172 | A sequence of table names that were found in B but weren't in | |
171 | A. |
|
173 | A. | |
172 |
|
174 | |||
173 | .. attribute:: tables_missing_from_B |
|
175 | .. attribute:: tables_missing_from_B | |
174 |
|
176 | |||
175 | A sequence of table names that were found in A but weren't in |
|
177 | A sequence of table names that were found in A but weren't in | |
176 | B. |
|
178 | B. | |
177 |
|
179 | |||
178 | .. attribute:: tables_different |
|
180 | .. attribute:: tables_different | |
179 |
|
181 | |||
180 | A dictionary containing information about tables that were found |
|
182 | A dictionary containing information about tables that were found | |
181 | to be different. |
|
183 | to be different. | |
182 | It maps table names to a :class:`TableDiff` objects describing the |
|
184 | It maps table names to a :class:`TableDiff` objects describing the | |
183 | differences found. |
|
185 | differences found. | |
184 | """ |
|
186 | """ | |
185 |
|
187 | |||
186 | def __init__(self, |
|
188 | def __init__(self, | |
187 | metadataA, metadataB, |
|
189 | metadataA, metadataB, | |
188 | labelA='metadataA', |
|
190 | labelA='metadataA', | |
189 | labelB='metadataB', |
|
191 | labelB='metadataB', | |
190 | excludeTables=None): |
|
192 | excludeTables=None): | |
191 |
|
193 | |||
192 | self.metadataA, self.metadataB = metadataA, metadataB |
|
194 | self.metadataA, self.metadataB = metadataA, metadataB | |
193 | self.labelA, self.labelB = labelA, labelB |
|
195 | self.labelA, self.labelB = labelA, labelB | |
194 | self.label_width = max(len(labelA),len(labelB)) |
|
196 | self.label_width = max(len(labelA),len(labelB)) | |
195 | excludeTables = set(excludeTables or []) |
|
197 | excludeTables = set(excludeTables or []) | |
196 |
|
198 | |||
197 | A_table_names = set(metadataA.tables.keys()) |
|
199 | A_table_names = set(metadataA.tables.keys()) | |
198 | B_table_names = set(metadataB.tables.keys()) |
|
200 | B_table_names = set(metadataB.tables.keys()) | |
199 |
|
201 | |||
200 | self.tables_missing_from_A = sorted( |
|
202 | self.tables_missing_from_A = sorted( | |
201 | B_table_names - A_table_names - excludeTables |
|
203 | B_table_names - A_table_names - excludeTables | |
202 | ) |
|
204 | ) | |
203 | self.tables_missing_from_B = sorted( |
|
205 | self.tables_missing_from_B = sorted( | |
204 | A_table_names - B_table_names - excludeTables |
|
206 | A_table_names - B_table_names - excludeTables | |
205 | ) |
|
207 | ) | |
206 |
|
208 | |||
207 | self.tables_different = {} |
|
209 | self.tables_different = {} | |
208 | for table_name in A_table_names.intersection(B_table_names): |
|
210 | for table_name in A_table_names.intersection(B_table_names): | |
209 |
|
211 | |||
210 | td = TableDiff() |
|
212 | td = TableDiff() | |
211 |
|
213 | |||
212 | A_table = metadataA.tables[table_name] |
|
214 | A_table = metadataA.tables[table_name] | |
213 | B_table = metadataB.tables[table_name] |
|
215 | B_table = metadataB.tables[table_name] | |
214 |
|
216 | |||
215 | A_column_names = set(A_table.columns.keys()) |
|
217 | A_column_names = set(A_table.columns.keys()) | |
216 | B_column_names = set(B_table.columns.keys()) |
|
218 | B_column_names = set(B_table.columns.keys()) | |
217 |
|
219 | |||
218 | td.columns_missing_from_A = sorted( |
|
220 | td.columns_missing_from_A = sorted( | |
219 | B_column_names - A_column_names |
|
221 | B_column_names - A_column_names | |
220 | ) |
|
222 | ) | |
221 |
|
223 | |||
222 | td.columns_missing_from_B = sorted( |
|
224 | td.columns_missing_from_B = sorted( | |
223 | A_column_names - B_column_names |
|
225 | A_column_names - B_column_names | |
224 | ) |
|
226 | ) | |
225 |
|
227 | |||
226 | td.columns_different = {} |
|
228 | td.columns_different = {} | |
227 |
|
229 | |||
228 | for col_name in A_column_names.intersection(B_column_names): |
|
230 | for col_name in A_column_names.intersection(B_column_names): | |
229 |
|
231 | |||
230 | cd = ColDiff( |
|
232 | cd = ColDiff( | |
231 | A_table.columns.get(col_name), |
|
233 | A_table.columns.get(col_name), | |
232 | B_table.columns.get(col_name) |
|
234 | B_table.columns.get(col_name) | |
233 | ) |
|
235 | ) | |
234 |
|
236 | |||
235 | if cd: |
|
237 | if cd: | |
236 | td.columns_different[col_name]=cd |
|
238 | td.columns_different[col_name]=cd | |
237 |
|
239 | |||
238 | # XXX - index and constraint differences should |
|
240 | # XXX - index and constraint differences should | |
239 | # be checked for here |
|
241 | # be checked for here | |
240 |
|
242 | |||
241 | if td: |
|
243 | if td: | |
242 | self.tables_different[table_name]=td |
|
244 | self.tables_different[table_name]=td | |
243 |
|
245 | |||
244 | def __str__(self): |
|
246 | def __str__(self): | |
245 | ''' Summarize differences. ''' |
|
247 | ''' Summarize differences. ''' | |
246 | out = [] |
|
248 | out = [] | |
247 | column_template =' %%%is: %%r' % self.label_width |
|
249 | column_template =' %%%is: %%r' % self.label_width | |
248 |
|
250 | |||
249 | for names,label in ( |
|
251 | for names,label in ( | |
250 | (self.tables_missing_from_A,self.labelA), |
|
252 | (self.tables_missing_from_A,self.labelA), | |
251 | (self.tables_missing_from_B,self.labelB), |
|
253 | (self.tables_missing_from_B,self.labelB), | |
252 | ): |
|
254 | ): | |
253 | if names: |
|
255 | if names: | |
254 | out.append( |
|
256 | out.append( | |
255 | ' tables missing from %s: %s' % ( |
|
257 | ' tables missing from %s: %s' % ( | |
256 | label,', '.join(sorted(names)) |
|
258 | label,', '.join(sorted(names)) | |
257 | ) |
|
259 | ) | |
258 | ) |
|
260 | ) | |
259 |
|
261 | |||
260 | for name,td in sorted(self.tables_different.items()): |
|
262 | for name,td in sorted(self.tables_different.items()): | |
261 | out.append( |
|
263 | out.append( | |
262 | ' table with differences: %s' % name |
|
264 | ' table with differences: %s' % name | |
263 | ) |
|
265 | ) | |
264 | for names,label in ( |
|
266 | for names,label in ( | |
265 | (td.columns_missing_from_A,self.labelA), |
|
267 | (td.columns_missing_from_A,self.labelA), | |
266 | (td.columns_missing_from_B,self.labelB), |
|
268 | (td.columns_missing_from_B,self.labelB), | |
267 | ): |
|
269 | ): | |
268 | if names: |
|
270 | if names: | |
269 | out.append( |
|
271 | out.append( | |
270 | ' %s missing these columns: %s' % ( |
|
272 | ' %s missing these columns: %s' % ( | |
271 | label,', '.join(sorted(names)) |
|
273 | label,', '.join(sorted(names)) | |
272 | ) |
|
274 | ) | |
273 | ) |
|
275 | ) | |
274 | for name,cd in td.columns_different.items(): |
|
276 | for name,cd in td.columns_different.items(): | |
275 | out.append(' column with differences: %s' % name) |
|
277 | out.append(' column with differences: %s' % name) | |
276 | out.append(column_template % (self.labelA,cd.col_A)) |
|
278 | out.append(column_template % (self.labelA,cd.col_A)) | |
277 | out.append(column_template % (self.labelB,cd.col_B)) |
|
279 | out.append(column_template % (self.labelB,cd.col_B)) | |
278 |
|
280 | |||
279 | if out: |
|
281 | if out: | |
280 | out.insert(0, 'Schema diffs:') |
|
282 | out.insert(0, 'Schema diffs:') | |
281 | return '\n'.join(out) |
|
283 | return '\n'.join(out) | |
282 | else: |
|
284 | else: | |
283 | return 'No schema diffs' |
|
285 | return 'No schema diffs' | |
284 |
|
286 | |||
285 | def __len__(self): |
|
287 | def __len__(self): | |
286 | """ |
|
288 | """ | |
287 | Used in bool evaluation, return of 0 means no diffs. |
|
289 | Used in bool evaluation, return of 0 means no diffs. | |
288 | """ |
|
290 | """ | |
289 | return ( |
|
291 | return ( | |
290 | len(self.tables_missing_from_A) + |
|
292 | len(self.tables_missing_from_A) + | |
291 | len(self.tables_missing_from_B) + |
|
293 | len(self.tables_missing_from_B) + | |
292 | len(self.tables_different) |
|
294 | len(self.tables_different) | |
293 | ) |
|
295 | ) |
General Comments 0
You need to be logged in to leave comments.
Login now