##// END OF EJS Templates
sync sqlalchemy migrate with latest changes
marcink -
r3767:e203cd36 beta
parent child Browse files
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 exceptions as sa_exceptions
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, reflect=True)
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