comparison rhodecode/lib/dbmigrate/migrate/versioning/genmodel.py @ 1442:7f31de1584c6 beta

update migrations for 1.2
author Marcin Kuzminski <marcin@python-works.com>
date Sun, 14 Aug 2011 23:51:21 +0300
parents 6832ef664673
children cf51bbfb120e
comparison
equal deleted inserted replaced
1441:b596a0e63466 1442:7f31de1584c6
1 """ 1 """
2 Code to generate a Python model from a database or differences 2 Code to generate a Python model from a database or differences
3 between a model and database. 3 between a model and database.
4 4
5 Some of this is borrowed heavily from the AutoCode project at: 5 Some of this is borrowed heavily from the AutoCode project at:
6 http://code.google.com/p/sqlautocode/ 6 http://code.google.com/p/sqlautocode/
7 """ 7 """
8 8
9 import sys 9 import sys
10 import logging 10 import logging
11 11
12 import sqlalchemy 12 import sqlalchemy
13 13
14 from rhodecode.lib.dbmigrate import migrate 14 from rhodecode.lib.dbmigrate import migrate
15 from rhodecode.lib.dbmigrate.migrate import changeset 15 from rhodecode.lib.dbmigrate.migrate import changeset
16
16 17
17 log = logging.getLogger(__name__) 18 log = logging.getLogger(__name__)
18 HEADER = """ 19 HEADER = """
19 ## File autogenerated by genmodel.py 20 ## File autogenerated by genmodel.py
20 21
31 Base = declarative.declarative_base() 32 Base = declarative.declarative_base()
32 """ 33 """
33 34
34 35
35 class ModelGenerator(object): 36 class ModelGenerator(object):
37 """Various transformations from an A, B diff.
38
39 In the implementation, A tends to be called the model and B
40 the database (although this is not true of all diffs).
41 The diff is directionless, but transformations apply the diff
42 in a particular direction, described in the method name.
43 """
36 44
37 def __init__(self, diff, engine, declarative=False): 45 def __init__(self, diff, engine, declarative=False):
38 self.diff = diff 46 self.diff = diff
39 self.engine = engine 47 self.engine = engine
40 self.declarative = declarative 48 self.declarative = declarative
56 # default value for the sequence, but let's not show 64 # default value for the sequence, but let's not show
57 # that. 65 # that.
58 pass 66 pass
59 else: 67 else:
60 kwarg.append('default') 68 kwarg.append('default')
61 ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg) 69 args = ['%s=%r' % (k, getattr(col, k)) for k in kwarg]
62 70
63 # crs: not sure if this is good idea, but it gets rid of extra 71 # crs: not sure if this is good idea, but it gets rid of extra
64 # u'' 72 # u''
65 name = col.name.encode('utf8') 73 name = col.name.encode('utf8')
66 74
70 not cls.__name__.isupper(): 78 not cls.__name__.isupper():
71 if cls is not type_.__class__: 79 if cls is not type_.__class__:
72 type_ = cls() 80 type_ = cls()
73 break 81 break
74 82
83 type_repr = repr(type_)
84 if type_repr.endswith('()'):
85 type_repr = type_repr[:-2]
86
87 constraints = [repr(cn) for cn in col.constraints]
88
75 data = { 89 data = {
76 'name': name, 90 'name': name,
77 'type': type_, 91 'commonStuff': ', '.join([type_repr] + constraints + args),
78 'constraints': ', '.join([repr(cn) for cn in col.constraints]), 92 }
79 'args': ks and ks or ''} 93
80
81 if data['constraints']:
82 if data['args']:
83 data['args'] = ',' + data['args']
84
85 if data['constraints'] or data['args']:
86 data['maybeComma'] = ','
87 else:
88 data['maybeComma'] = ''
89
90 commonStuff = """ %(maybeComma)s %(constraints)s %(args)s)""" % data
91 commonStuff = commonStuff.strip()
92 data['commonStuff'] = commonStuff
93 if self.declarative: 94 if self.declarative:
94 return """%(name)s = Column(%(type)r%(commonStuff)s""" % data 95 return """%(name)s = Column(%(commonStuff)s)""" % data
95 else: 96 else:
96 return """Column(%(name)r, %(type)r%(commonStuff)s""" % data 97 return """Column(%(name)r, %(commonStuff)s)""" % data
97 98
98 def getTableDefn(self, table): 99 def _getTableDefn(self, table, metaName='meta'):
99 out = [] 100 out = []
100 tableName = table.name 101 tableName = table.name
101 if self.declarative: 102 if self.declarative:
102 out.append("class %(table)s(Base):" % {'table': tableName}) 103 out.append("class %(table)s(Base):" % {'table': tableName})
103 out.append(" __tablename__ = '%(table)s'" % {'table': tableName}) 104 out.append(" __tablename__ = '%(table)s'\n" %
105 {'table': tableName})
104 for col in table.columns: 106 for col in table.columns:
105 out.append(" %s" % self.column_repr(col)) 107 out.append(" %s" % self.column_repr(col))
106 else: 108 out.append('\n')
107 out.append("%(table)s = Table('%(table)s', meta," % \ 109 else:
108 {'table': tableName}) 110 out.append("%(table)s = Table('%(table)s', %(meta)s," %
111 {'table': tableName, 'meta': metaName})
109 for col in table.columns: 112 for col in table.columns:
110 out.append(" %s," % self.column_repr(col)) 113 out.append(" %s," % self.column_repr(col))
111 out.append(")") 114 out.append(")\n")
112 return out 115 return out
113 116
114 def _get_tables(self,missingA=False,missingB=False,modified=False): 117 def _get_tables(self,missingA=False,missingB=False,modified=False):
115 to_process = [] 118 to_process = []
116 for bool_,names,metadata in ( 119 for bool_,names,metadata in (
120 ): 123 ):
121 if bool_: 124 if bool_:
122 for name in names: 125 for name in names:
123 yield metadata.tables.get(name) 126 yield metadata.tables.get(name)
124 127
125 def toPython(self): 128 def genBDefinition(self):
126 """Assume database is current and model is empty.""" 129 """Generates the source code for a definition of B.
130
131 Assumes a diff where A is empty.
132
133 Was: toPython. Assume database (B) is current and model (A) is empty.
134 """
135
127 out = [] 136 out = []
128 if self.declarative: 137 if self.declarative:
129 out.append(DECLARATIVE_HEADER) 138 out.append(DECLARATIVE_HEADER)
130 else: 139 else:
131 out.append(HEADER) 140 out.append(HEADER)
132 out.append("") 141 out.append("")
133 for table in self._get_tables(missingA=True): 142 for table in self._get_tables(missingA=True):
134 out.extend(self.getTableDefn(table)) 143 out.extend(self._getTableDefn(table))
135 out.append("")
136 return '\n'.join(out) 144 return '\n'.join(out)
137 145
138 def toUpgradeDowngradePython(self, indent=' '): 146 def genB2AMigration(self, indent=' '):
139 ''' Assume model is most current and database is out-of-date. ''' 147 '''Generate a migration from B to A.
140 decls = ['from rhodecode.lib.dbmigrate.migrate.changeset import schema', 148
141 'meta = MetaData()'] 149 Was: toUpgradeDowngradePython
142 for table in self._get_tables( 150 Assume model (A) is most current and database (B) is out-of-date.
143 missingA=True,missingB=True,modified=True 151 '''
144 ): 152
145 decls.extend(self.getTableDefn(table)) 153 decls = ['from migrate.changeset import schema',
146 154 'pre_meta = MetaData()',
147 upgradeCommands, downgradeCommands = [], [] 155 'post_meta = MetaData()',
148 for tableName in self.diff.tables_missing_from_A: 156 ]
149 upgradeCommands.append("%(table)s.drop()" % {'table': tableName}) 157 upgradeCommands = ['pre_meta.bind = migrate_engine',
150 downgradeCommands.append("%(table)s.create()" % \ 158 'post_meta.bind = migrate_engine']
151 {'table': tableName}) 159 downgradeCommands = list(upgradeCommands)
152 for tableName in self.diff.tables_missing_from_B: 160
153 upgradeCommands.append("%(table)s.create()" % {'table': tableName}) 161 for tn in self.diff.tables_missing_from_A:
154 downgradeCommands.append("%(table)s.drop()" % {'table': tableName}) 162 pre_table = self.diff.metadataB.tables[tn]
155 163 decls.extend(self._getTableDefn(pre_table, metaName='pre_meta'))
156 for tableName in self.diff.tables_different: 164 upgradeCommands.append(
157 dbTable = self.diff.metadataB.tables[tableName] 165 "pre_meta.tables[%(table)r].drop()" % {'table': tn})
158 missingInDatabase, missingInModel, diffDecl = \ 166 downgradeCommands.append(
159 self.diff.colDiffs[tableName] 167 "pre_meta.tables[%(table)r].create()" % {'table': tn})
160 for col in missingInDatabase: 168
161 upgradeCommands.append('%s.columns[%r].create()' % ( 169 for tn in self.diff.tables_missing_from_B:
162 modelTable, col.name)) 170 post_table = self.diff.metadataA.tables[tn]
163 downgradeCommands.append('%s.columns[%r].drop()' % ( 171 decls.extend(self._getTableDefn(post_table, metaName='post_meta'))
164 modelTable, col.name)) 172 upgradeCommands.append(
165 for col in missingInModel: 173 "post_meta.tables[%(table)r].create()" % {'table': tn})
166 upgradeCommands.append('%s.columns[%r].drop()' % ( 174 downgradeCommands.append(
167 modelTable, col.name)) 175 "post_meta.tables[%(table)r].drop()" % {'table': tn})
168 downgradeCommands.append('%s.columns[%r].create()' % ( 176
169 modelTable, col.name)) 177 for (tn, td) in self.diff.tables_different.iteritems():
170 for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl: 178 if td.columns_missing_from_A or td.columns_different:
179 pre_table = self.diff.metadataB.tables[tn]
180 decls.extend(self._getTableDefn(
181 pre_table, metaName='pre_meta'))
182 if td.columns_missing_from_B or td.columns_different:
183 post_table = self.diff.metadataA.tables[tn]
184 decls.extend(self._getTableDefn(
185 post_table, metaName='post_meta'))
186
187 for col in td.columns_missing_from_A:
188 upgradeCommands.append(
189 'pre_meta.tables[%r].columns[%r].drop()' % (tn, col))
190 downgradeCommands.append(
191 'pre_meta.tables[%r].columns[%r].create()' % (tn, col))
192 for col in td.columns_missing_from_B:
193 upgradeCommands.append(
194 'post_meta.tables[%r].columns[%r].create()' % (tn, col))
195 downgradeCommands.append(
196 'post_meta.tables[%r].columns[%r].drop()' % (tn, col))
197 for modelCol, databaseCol, modelDecl, databaseDecl in td.columns_different:
171 upgradeCommands.append( 198 upgradeCommands.append(
172 'assert False, "Can\'t alter columns: %s:%s=>%s"' % ( 199 'assert False, "Can\'t alter columns: %s:%s=>%s"' % (
173 modelTable, modelCol.name, databaseCol.name)) 200 tn, modelCol.name, databaseCol.name))
174 downgradeCommands.append( 201 downgradeCommands.append(
175 'assert False, "Can\'t alter columns: %s:%s=>%s"' % ( 202 'assert False, "Can\'t alter columns: %s:%s=>%s"' % (
176 modelTable, modelCol.name, databaseCol.name)) 203 tn, modelCol.name, databaseCol.name))
177 pre_command = ' meta.bind = migrate_engine'
178 204
179 return ( 205 return (
180 '\n'.join(decls), 206 '\n'.join(decls),
181 '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in upgradeCommands]), 207 '\n'.join('%s%s' % (indent, line) for line in upgradeCommands),
182 '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in downgradeCommands])) 208 '\n'.join('%s%s' % (indent, line) for line in downgradeCommands))
183 209
184 def _db_can_handle_this_change(self,td): 210 def _db_can_handle_this_change(self,td):
211 """Check if the database can handle going from B to A."""
212
185 if (td.columns_missing_from_B 213 if (td.columns_missing_from_B
186 and not td.columns_missing_from_A 214 and not td.columns_missing_from_A
187 and not td.columns_different): 215 and not td.columns_different):
188 # Even sqlite can handle this. 216 # Even sqlite can handle column additions.
189 return True 217 return True
190 else: 218 else:
191 return not self.engine.url.drivername.startswith('sqlite') 219 return not self.engine.url.drivername.startswith('sqlite')
192 220
193 def applyModel(self): 221 def runB2A(self):
194 """Apply model to current database.""" 222 """Goes from B to A.
223
224 Was: applyModel. Apply model (A) to current database (B).
225 """
195 226
196 meta = sqlalchemy.MetaData(self.engine) 227 meta = sqlalchemy.MetaData(self.engine)
197 228
198 for table in self._get_tables(missingA=True): 229 for table in self._get_tables(missingA=True):
199 table = table.tometadata(meta) 230 table = table.tometadata(meta)
249 connection.execute('DROP TABLE %s' % tempName) 280 connection.execute('DROP TABLE %s' % tempName)
250 trans.commit() 281 trans.commit()
251 except: 282 except:
252 trans.rollback() 283 trans.rollback()
253 raise 284 raise
285