##// END OF EJS Templates
db-migrate: backport some changes for py3 compat.
marcink -
r4343:2ba995a1 default
parent child Browse files
Show More
@@ -1,30 +1,31 b''
1 1 """
2 2 This module extends SQLAlchemy and provides additional DDL [#]_
3 3 support.
4 4
5 5 .. [#] SQL Data Definition Language
6 6 """
7 7 import re
8 8 import warnings
9 9
10 10 import sqlalchemy
11 11 from sqlalchemy import __version__ as _sa_version
12 12
13 13 warnings.simplefilter('always', DeprecationWarning)
14 14
15 _sa_version = tuple(int(re.match("\d+", x).group(0))
16 for x in _sa_version.split("."))
15 _sa_version = tuple(int(re.match("\d+", x).group(0)) for x in _sa_version.split("."))
17 16 SQLA_07 = _sa_version >= (0, 7)
18 17 SQLA_08 = _sa_version >= (0, 8)
18 SQLA_09 = _sa_version >= (0, 9)
19 SQLA_10 = _sa_version >= (1, 0)
19 20
20 21 del re
21 22 del _sa_version
22 23
23 24 from rhodecode.lib.dbmigrate.migrate.changeset.schema import *
24 25 from rhodecode.lib.dbmigrate.migrate.changeset.constraint import *
25 26
26 sqlalchemy.schema.Table.__bases__ += (ChangesetTable,)
27 sqlalchemy.schema.Column.__bases__ += (ChangesetColumn,)
28 sqlalchemy.schema.Index.__bases__ += (ChangesetIndex,)
27 sqlalchemy.schema.Table.__bases__ += (ChangesetTable, )
28 sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, )
29 sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, )
29 30
30 sqlalchemy.schema.DefaultClause.__bases__ += (ChangesetDefaultClause,)
31 sqlalchemy.schema.DefaultClause.__bases__ += (ChangesetDefaultClause, )
@@ -1,315 +1,314 b''
1 1 """
2 2 Extensions to SQLAlchemy for altering existing tables.
3 3
4 4 At the moment, this isn't so much based off of ANSI as much as
5 5 things that just happen to work with multiple databases.
6 6 """
7 7 import StringIO
8 8
9 9 import sqlalchemy as sa
10 10 from sqlalchemy.schema import SchemaVisitor
11 11 from sqlalchemy.engine.default import DefaultDialect
12 12 from sqlalchemy.sql import ClauseElement
13 13 from sqlalchemy.schema import (ForeignKeyConstraint,
14 14 PrimaryKeyConstraint,
15 15 CheckConstraint,
16 16 UniqueConstraint,
17 17 Index)
18 18
19 19 import sqlalchemy.sql.compiler
20 20 from rhodecode.lib.dbmigrate.migrate import exceptions
21 21 from rhodecode.lib.dbmigrate.migrate.changeset import constraint
22 22 from rhodecode.lib.dbmigrate.migrate.changeset import util
23 23
24 24 from sqlalchemy.schema import AddConstraint, DropConstraint
25 25 from sqlalchemy.sql.compiler import DDLCompiler
26 26 SchemaGenerator = SchemaDropper = DDLCompiler
27 27
28 28
29 29 class AlterTableVisitor(SchemaVisitor):
30 30 """Common operations for ``ALTER TABLE`` statements."""
31 31
32 32 # engine.Compiler looks for .statement
33 33 # when it spawns off a new compiler
34 34 statement = ClauseElement()
35 35
36 36 def append(self, s):
37 37 """Append content to the SchemaIterator's query buffer."""
38 38
39 39 self.buffer.write(s)
40 40
41 41 def execute(self):
42 42 """Execute the contents of the SchemaIterator's buffer."""
43 43 try:
44 44 return self.connection.execute(self.buffer.getvalue())
45 45 finally:
46 46 self.buffer.seek(0)
47 47 self.buffer.truncate()
48 48
49 49 def __init__(self, dialect, connection, **kw):
50 50 self.connection = connection
51 51 self.buffer = StringIO.StringIO()
52 52 self.preparer = dialect.identifier_preparer
53 53 self.dialect = dialect
54 54
55 55 def traverse_single(self, elem):
56 56 ret = super(AlterTableVisitor, self).traverse_single(elem)
57 57 if ret:
58 58 # adapt to 0.6 which uses a string-returning
59 59 # object
60 60 self.append(" %s" % ret)
61 61
62 62 def _to_table(self, param):
63 63 """Returns the table object for the given param object."""
64 64 if isinstance(param, (sa.Column, sa.Index, sa.schema.Constraint)):
65 65 ret = param.table
66 66 else:
67 67 ret = param
68 68 return ret
69 69
70 70 def start_alter_table(self, param):
71 71 """Returns the start of an ``ALTER TABLE`` SQL-Statement.
72 72
73 73 Use the param object to determine the table name and use it
74 74 for building the SQL statement.
75 75
76 76 :param param: object to determine the table from
77 77 :type param: :class:`sqlalchemy.Column`, :class:`sqlalchemy.Index`,
78 78 :class:`sqlalchemy.schema.Constraint`, :class:`sqlalchemy.Table`,
79 79 or string (table name)
80 80 """
81 81 table = self._to_table(param)
82 82 self.append('\nALTER TABLE %s ' % self.preparer.format_table(table))
83 83 return table
84 84
85 85
86 86 class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator):
87 87 """Extends ansisql generator for column creation (alter table add col)"""
88 88
89 89 def visit_column(self, column):
90 90 """Create a column (table already exists).
91 91
92 92 :param column: column object
93 93 :type column: :class:`sqlalchemy.Column` instance
94 94 """
95 95 if column.default is not None:
96 96 self.traverse_single(column.default)
97 97
98 98 table = self.start_alter_table(column)
99 99 self.append("ADD ")
100
101 100 self.append(self.get_column_specification(column))
102 101
103 102 for cons in column.constraints:
104 103 self.traverse_single(cons)
105 104 self.execute()
106 105
107 106 # ALTER TABLE STATEMENTS
108 107
109 108 # add indexes and unique constraints
110 109 if column.index_name:
111 110 Index(column.index_name,column).create()
112 111 elif column.unique_name:
113 112 constraint.UniqueConstraint(column,
114 113 name=column.unique_name).create()
115 114
116 115 # SA bounds FK constraints to table, add manually
117 116 for fk in column.foreign_keys:
118 117 self.add_foreignkey(fk.constraint)
119 118
120 119 # add primary key constraint if needed
121 120 if column.primary_key_name:
122 121 cons = constraint.PrimaryKeyConstraint(column,
123 122 name=column.primary_key_name)
124 123 cons.create()
125 124
126 125 def add_foreignkey(self, fk):
127 126 self.connection.execute(AddConstraint(fk))
128 127
129 128 class ANSIColumnDropper(AlterTableVisitor, SchemaDropper):
130 129 """Extends ANSI SQL dropper for column dropping (``ALTER TABLE
131 130 DROP COLUMN``).
132 131 """
133 132
134 133 def visit_column(self, column):
135 134 """Drop a column from its table.
136 135
137 136 :param column: the column object
138 137 :type column: :class:`sqlalchemy.Column`
139 138 """
140 139 table = self.start_alter_table(column)
141 140 self.append('DROP COLUMN %s' % self.preparer.format_column(column))
142 141 self.execute()
143 142
144 143
145 144 class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
146 145 """Manages changes to existing schema elements.
147 146
148 147 Note that columns are schema elements; ``ALTER TABLE ADD COLUMN``
149 148 is in SchemaGenerator.
150 149
151 150 All items may be renamed. Columns can also have many of their properties -
152 151 type, for example - changed.
153 152
154 153 Each function is passed a tuple, containing (object, name); where
155 154 object is a type of object you'd expect for that function
156 155 (ie. table for visit_table) and name is the object's new
157 156 name. NONE means the name is unchanged.
158 157 """
159 158
160 159 def visit_table(self, table):
161 160 """Rename a table. Other ops aren't supported."""
162 161 self.start_alter_table(table)
163 162 q = util.safe_quote(table)
164 163 self.append("RENAME TO %s" % self.preparer.quote(table.new_name, q))
165 164 self.execute()
166 165
167 166 def visit_index(self, index):
168 167 """Rename an index"""
169 168 if hasattr(self, '_validate_identifier'):
170 169 # SA <= 0.6.3
171 170 self.append("ALTER INDEX %s RENAME TO %s" % (
172 171 self.preparer.quote(
173 172 self._validate_identifier(
174 173 index.name, True), index.quote),
175 174 self.preparer.quote(
176 175 self._validate_identifier(
177 176 index.new_name, True), index.quote)))
178 177 elif hasattr(self, '_index_identifier'):
179 178 # SA >= 0.6.5, < 0.8
180 179 self.append("ALTER INDEX %s RENAME TO %s" % (
181 180 self.preparer.quote(
182 181 self._index_identifier(
183 182 index.name), index.quote),
184 183 self.preparer.quote(
185 184 self._index_identifier(
186 185 index.new_name), index.quote)))
187 186 else:
188 187 # SA >= 0.8
189 188 class NewName(object):
190 189 """Map obj.name -> obj.new_name"""
191 190 def __init__(self, index):
192 191 self.name = index.new_name
193 192 self._obj = index
194 193
195 194 def __getattr__(self, attr):
196 195 if attr == 'name':
197 196 return getattr(self, attr)
198 197 return getattr(self._obj, attr)
199 198
200 199 self.append("ALTER INDEX %s RENAME TO %s" % (
201 200 self._prepared_index_name(index),
202 201 self._prepared_index_name(NewName(index))))
203 202
204 203 self.execute()
205 204
206 205 def visit_column(self, delta):
207 206 """Rename/change a column."""
208 207 # ALTER COLUMN is implemented as several ALTER statements
209 208 keys = delta.keys()
210 209 if 'type' in keys:
211 210 self._run_subvisit(delta, self._visit_column_type)
212 211 if 'nullable' in keys:
213 212 self._run_subvisit(delta, self._visit_column_nullable)
214 213 if 'server_default' in keys:
215 214 # Skip 'default': only handle server-side defaults, others
216 215 # are managed by the app, not the db.
217 216 self._run_subvisit(delta, self._visit_column_default)
218 217 if 'name' in keys:
219 218 self._run_subvisit(delta, self._visit_column_name, start_alter=False)
220 219
221 220 def _run_subvisit(self, delta, func, start_alter=True):
222 221 """Runs visit method based on what needs to be changed on column"""
223 222 table = self._to_table(delta.table)
224 223 col_name = delta.current_name
225 224 if start_alter:
226 225 self.start_alter_column(table, col_name)
227 226 ret = func(table, delta.result_column, delta)
228 227 self.execute()
229 228
230 229 def start_alter_column(self, table, col_name):
231 230 """Starts ALTER COLUMN"""
232 231 self.start_alter_table(table)
233 232 q = util.safe_quote(table)
234 233 self.append("ALTER COLUMN %s " % self.preparer.quote(col_name, q))
235 234
236 235 def _visit_column_nullable(self, table, column, delta):
237 236 nullable = delta['nullable']
238 237 if nullable:
239 238 self.append("DROP NOT NULL")
240 239 else:
241 240 self.append("SET NOT NULL")
242 241
243 242 def _visit_column_default(self, table, column, delta):
244 243 default_text = self.get_column_default_string(column)
245 244 if default_text is not None:
246 245 self.append("SET DEFAULT %s" % default_text)
247 246 else:
248 247 self.append("DROP DEFAULT")
249 248
250 249 def _visit_column_type(self, table, column, delta):
251 250 type_ = delta['type']
252 251 type_text = str(type_.compile(dialect=self.dialect))
253 252 self.append("TYPE %s" % type_text)
254 253
255 254 def _visit_column_name(self, table, column, delta):
256 255 self.start_alter_table(table)
257 256 q = util.safe_quote(table)
258 257 col_name = self.preparer.quote(delta.current_name, q)
259 258 new_name = self.preparer.format_column(delta.result_column)
260 259 self.append('RENAME COLUMN %s TO %s' % (col_name, new_name))
261 260
262 261
263 262 class ANSIConstraintCommon(AlterTableVisitor):
264 263 """
265 264 Migrate's constraints require a separate creation function from
266 265 SA's: Migrate's constraints are created independently of a table;
267 266 SA's are created at the same time as the table.
268 267 """
269 268
270 269 def get_constraint_name(self, cons):
271 270 """Gets a name for the given constraint.
272 271
273 272 If the name is already set it will be used otherwise the
274 273 constraint's :meth:`autoname <migrate.changeset.constraint.ConstraintChangeset.autoname>`
275 274 method is used.
276 275
277 276 :param cons: constraint object
278 277 """
279 278 if cons.name is not None:
280 279 ret = cons.name
281 280 else:
282 281 ret = cons.name = cons.autoname()
283 282 return self.preparer.quote(ret, cons.quote)
284 283
285 284 def visit_migrate_primary_key_constraint(self, *p, **k):
286 285 self._visit_constraint(*p, **k)
287 286
288 287 def visit_migrate_foreign_key_constraint(self, *p, **k):
289 288 self._visit_constraint(*p, **k)
290 289
291 290 def visit_migrate_check_constraint(self, *p, **k):
292 291 self._visit_constraint(*p, **k)
293 292
294 293 def visit_migrate_unique_constraint(self, *p, **k):
295 294 self._visit_constraint(*p, **k)
296 295
297 296 class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
298 297 def _visit_constraint(self, constraint):
299 298 constraint.name = self.get_constraint_name(constraint)
300 299 self.append(self.process(AddConstraint(constraint)))
301 300 self.execute()
302 301
303 302 class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
304 303 def _visit_constraint(self, constraint):
305 304 constraint.name = self.get_constraint_name(constraint)
306 305 self.append(self.process(DropConstraint(constraint, cascade=constraint.cascade)))
307 306 self.execute()
308 307
309 308
310 309 class ANSIDialect(DefaultDialect):
311 310 columngenerator = ANSIColumnGenerator
312 311 columndropper = ANSIColumnDropper
313 312 schemachanger = ANSISchemaChanger
314 313 constraintgenerator = ANSIConstraintGenerator
315 314 constraintdropper = ANSIConstraintDropper
@@ -1,200 +1,200 b''
1 1 """
2 2 This module defines standalone schema constraint classes.
3 3 """
4 4 from sqlalchemy import schema
5 5
6 6 from rhodecode.lib.dbmigrate.migrate.exceptions import *
7 7
8 8
9 9 class ConstraintChangeset(object):
10 10 """Base class for Constraint classes."""
11 11
12 12 def _normalize_columns(self, cols, table_name=False):
13 13 """Given: column objects or names; return col names and
14 14 (maybe) a table"""
15 15 colnames = []
16 16 table = None
17 17 for col in cols:
18 18 if isinstance(col, schema.Column):
19 19 if col.table is not None and table is None:
20 20 table = col.table
21 21 if table_name:
22 22 col = '.'.join((col.table.name, col.name))
23 23 else:
24 24 col = col.name
25 25 colnames.append(col)
26 26 return colnames, table
27 27
28 28 def __do_imports(self, visitor_name, *a, **kw):
29 29 engine = kw.pop('engine', self.table.bind)
30 30 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (
31 31 get_engine_visitor, run_single_visitor)
32 32 visitorcallable = get_engine_visitor(engine, visitor_name)
33 33 run_single_visitor(engine, visitorcallable, self, *a, **kw)
34 34
35 35 def create(self, *a, **kw):
36 36 """Create the constraint in the database.
37 37
38 38 :param engine: the database engine to use. If this is \
39 39 :keyword:`None` the instance's engine will be used
40 40 :type engine: :class:`sqlalchemy.engine.base.Engine`
41 41 :param connection: reuse connection istead of creating new one.
42 42 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
43 43 """
44 44 # TODO: set the parent here instead of in __init__
45 45 self.__do_imports('constraintgenerator', *a, **kw)
46 46
47 47 def drop(self, *a, **kw):
48 48 """Drop the constraint from the database.
49 49
50 50 :param engine: the database engine to use. If this is
51 51 :keyword:`None` the instance's engine will be used
52 52 :param cascade: Issue CASCADE drop if database supports it
53 53 :type engine: :class:`sqlalchemy.engine.base.Engine`
54 54 :type cascade: bool
55 55 :param connection: reuse connection istead of creating new one.
56 56 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
57 57 :returns: Instance with cleared columns
58 58 """
59 59 self.cascade = kw.pop('cascade', False)
60 60 self.__do_imports('constraintdropper', *a, **kw)
61 61 # the spirit of Constraint objects is that they
62 62 # are immutable (just like in a DB. they're only ADDed
63 63 # or DROPped).
64 64 #self.columns.clear()
65 65 return self
66 66
67 67
68 68 class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint):
69 69 """Construct PrimaryKeyConstraint
70 70
71 71 Migrate's additional parameters:
72 72
73 73 :param cols: Columns in constraint.
74 74 :param table: If columns are passed as strings, this kw is required
75 75 :type table: Table instance
76 76 :type cols: strings or Column instances
77 77 """
78 78
79 79 __migrate_visit_name__ = 'migrate_primary_key_constraint'
80 80
81 81 def __init__(self, *cols, **kwargs):
82 82 colnames, table = self._normalize_columns(cols)
83 83 table = kwargs.pop('table', table)
84 84 super(PrimaryKeyConstraint, self).__init__(*colnames, **kwargs)
85 85 if table is not None:
86 86 self._set_parent(table)
87 87
88 88 def autoname(self):
89 89 """Mimic the database's automatic constraint names"""
90 90 return "%s_pkey" % self.table.name
91 91
92 92
93 93 class ForeignKeyConstraint(ConstraintChangeset, schema.ForeignKeyConstraint):
94 94 """Construct ForeignKeyConstraint
95 95
96 96 Migrate's additional parameters:
97 97
98 98 :param columns: Columns in constraint
99 99 :param refcolumns: Columns that this FK reffers to in another table.
100 100 :param table: If columns are passed as strings, this kw is required
101 101 :type table: Table instance
102 102 :type columns: list of strings or Column instances
103 103 :type refcolumns: list of strings or Column instances
104 104 """
105 105
106 106 __migrate_visit_name__ = 'migrate_foreign_key_constraint'
107 107
108 108 def __init__(self, columns, refcolumns, *args, **kwargs):
109 109 colnames, table = self._normalize_columns(columns)
110 110 table = kwargs.pop('table', table)
111 111 refcolnames, reftable = self._normalize_columns(refcolumns,
112 112 table_name=True)
113 113 super(ForeignKeyConstraint, self).__init__(
114 colnames, refcolnames, *args,**kwargs
114 colnames, refcolnames, *args, **kwargs
115 115 )
116 116 if table is not None:
117 117 self._set_parent(table)
118 118
119 119 @property
120 120 def referenced(self):
121 121 return [e.column for e in self.elements]
122 122
123 123 @property
124 124 def reftable(self):
125 125 return self.referenced[0].table
126 126
127 127 def autoname(self):
128 128 """Mimic the database's automatic constraint names"""
129 129 if hasattr(self.columns, 'keys'):
130 130 # SA <= 0.5
131 131 firstcol = self.columns[self.columns.keys()[0]]
132 132 ret = "%(table)s_%(firstcolumn)s_fkey" % {
133 133 'table': firstcol.table.name,
134 134 'firstcolumn': firstcol.name,}
135 135 else:
136 136 # SA >= 0.6
137 137 ret = "%(table)s_%(firstcolumn)s_fkey" % {
138 138 'table': self.table.name,
139 139 'firstcolumn': self.columns[0],}
140 140 return ret
141 141
142 142
143 143 class CheckConstraint(ConstraintChangeset, schema.CheckConstraint):
144 144 """Construct CheckConstraint
145 145
146 146 Migrate's additional parameters:
147 147
148 148 :param sqltext: Plain SQL text to check condition
149 149 :param columns: If not name is applied, you must supply this kw\
150 150 to autoname constraint
151 151 :param table: If columns are passed as strings, this kw is required
152 152 :type table: Table instance
153 153 :type columns: list of Columns instances
154 154 :type sqltext: string
155 155 """
156 156
157 157 __migrate_visit_name__ = 'migrate_check_constraint'
158 158
159 159 def __init__(self, sqltext, *args, **kwargs):
160 160 cols = kwargs.pop('columns', [])
161 161 if not cols and not kwargs.get('name', False):
162 162 raise InvalidConstraintError('You must either set "name"'
163 163 'parameter or "columns" to autogenarate it.')
164 164 colnames, table = self._normalize_columns(cols)
165 165 table = kwargs.pop('table', table)
166 166 schema.CheckConstraint.__init__(self, sqltext, *args, **kwargs)
167 167 if table is not None:
168 168 self._set_parent(table)
169 169 self.colnames = colnames
170 170
171 171 def autoname(self):
172 172 return "%(table)s_%(cols)s_check" % \
173 173 {'table': self.table.name, 'cols': "_".join(self.colnames)}
174 174
175 175
176 176 class UniqueConstraint(ConstraintChangeset, schema.UniqueConstraint):
177 177 """Construct UniqueConstraint
178 178
179 179 Migrate's additional parameters:
180 180
181 181 :param cols: Columns in constraint.
182 182 :param table: If columns are passed as strings, this kw is required
183 183 :type table: Table instance
184 184 :type cols: strings or Column instances
185 185
186 186 .. versionadded:: 0.6.0
187 187 """
188 188
189 189 __migrate_visit_name__ = 'migrate_unique_constraint'
190 190
191 191 def __init__(self, *cols, **kwargs):
192 192 self.colnames, table = self._normalize_columns(cols)
193 193 table = kwargs.pop('table', table)
194 194 super(UniqueConstraint, self).__init__(*self.colnames, **kwargs)
195 195 if table is not None:
196 196 self._set_parent(table)
197 197
198 198 def autoname(self):
199 199 """Mimic the database's automatic constraint names"""
200 200 return "%s_%s_key" % (self.table.name, '_'.join(self.colnames))
@@ -1,205 +1,209 b''
1 1 """
2 2 `SQLite`_ database specific implementations of changeset classes.
3 3
4 4 .. _`SQLite`: http://www.sqlite.org/
5 5 """
6 from UserDict import DictMixin
6 try: # Python 3
7 from collections.abc import MutableMapping as DictMixin
8 except ImportError: # Python 2
9 from UserDict import DictMixin
7 10 from copy import copy
8 11 import re
9 12
10 13 from sqlalchemy.databases import sqlite as sa_base
14 from sqlalchemy.schema import ForeignKeyConstraint
11 15 from sqlalchemy.schema import UniqueConstraint
12 16
13 17 from rhodecode.lib.dbmigrate.migrate import exceptions
14 18 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql
15 19 import sqlite3
16 20
17 21 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
18 22
19 23
20 24 class SQLiteCommon(object):
21 25
22 26 def _not_supported(self, op):
23 27 raise exceptions.NotSupportedError("SQLite does not support "
24 28 "%s; see http://www.sqlite.org/lang_altertable.html" % op)
25 29
26 30
27 31 class SQLiteHelper(SQLiteCommon):
28 32
29 33 def _get_unique_constraints(self, table):
30 34 """Retrieve information about existing unique constraints of the table
31 35
32 36 This feature is needed for recreate_table() to work properly.
33 37 """
34 38
35 39 data = table.metadata.bind.execute(
36 40 """SELECT sql
37 41 FROM sqlite_master
38 42 WHERE
39 43 type='table' AND
40 44 name=:table_name""",
41 45 table_name=table.name
42 46 ).fetchone()[0]
43 47
44 48 UNIQUE_PATTERN = "CONSTRAINT (\w+) UNIQUE \(([^\)]+)\)"
45 49 constraints = []
46 50 for name, cols in re.findall(UNIQUE_PATTERN, data):
47 51 # Filter out any columns that were dropped from the table.
48 52 columns = []
49 53 for c in cols.split(","):
50 54 if c in table.columns:
51 55 # There was a bug in reflection of SQLite columns with
52 56 # reserved identifiers as names (SQLite can return them
53 57 # wrapped with double quotes), so strip double quotes.
54 58 columns.extend(c.strip(' "'))
55 59 if columns:
56 60 constraints.extend(UniqueConstraint(*columns, name=name))
57 61 return constraints
58 62
59 63 def recreate_table(self, table, column=None, delta=None,
60 64 omit_uniques=None):
61 65 table_name = self.preparer.format_table(table)
62 66
63 67 # we remove all indexes so as not to have
64 68 # problems during copy and re-create
65 69 for index in table.indexes:
66 70 index.drop()
67 71
68 72 # reflect existing unique constraints
69 73 for uc in self._get_unique_constraints(table):
70 74 table.append_constraint(uc)
71 75 # omit given unique constraints when creating a new table if required
72 76 table.constraints = set([
73 77 cons for cons in table.constraints
74 78 if omit_uniques is None or cons.name not in omit_uniques
75 79 ])
76 80 tup = sqlite3.sqlite_version_info
77 81 if tup[0] > 3 or (tup[0] == 3 and tup[1] >= 26):
78 82 self.append('PRAGMA legacy_alter_table = ON')
79 83 self.execute()
80 84
81 85 self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
82 86 self.execute()
83 87 if tup[0] > 3 or (tup[0] == 3 and tup[1] >= 26):
84 88 self.append('PRAGMA legacy_alter_table = OFF')
85 89 self.execute()
86 90 insertion_string = self._modify_table(table, column, delta)
87 91
88 92 table.create(bind=self.connection)
89 93 self.append(insertion_string % {'table_name': table_name})
90 94 self.execute()
91 95 self.append('DROP TABLE migration_tmp')
92 96 self.execute()
93 97
94 98 def visit_column(self, delta):
95 99 if isinstance(delta, DictMixin):
96 100 column = delta.result_column
97 101 table = self._to_table(delta.table)
98 102 else:
99 103 column = delta
100 104 table = self._to_table(column.table)
101 105
102 106 self.recreate_table(table,column,delta)
103 107
104 108 class SQLiteColumnGenerator(SQLiteSchemaGenerator,
105 109 ansisql.ANSIColumnGenerator,
106 110 # at the end so we get the normal
107 111 # visit_column by default
108 112 SQLiteHelper,
109 113 SQLiteCommon
110 114 ):
111 115 """SQLite ColumnGenerator"""
112 116
113 117 def _modify_table(self, table, column, delta):
114 118 columns = ' ,'.join(map(
115 119 self.preparer.format_column,
116 120 [c for c in table.columns if c.name!=column.name]))
117 121 return ('INSERT INTO %%(table_name)s (%(cols)s) '
118 122 'SELECT %(cols)s from migration_tmp')%{'cols':columns}
119 123
120 124 def visit_column(self,column):
121 125 if column.foreign_keys:
122 126 SQLiteHelper.visit_column(self,column)
123 127 else:
124 128 super(SQLiteColumnGenerator,self).visit_column(column)
125 129
126 130 class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
127 131 """SQLite ColumnDropper"""
128 132
129 133 def _modify_table(self, table, column, delta):
130 134
131 135 columns = ' ,'.join(map(self.preparer.format_column, table.columns))
132 136 return 'INSERT INTO %(table_name)s SELECT ' + columns + \
133 137 ' from migration_tmp'
134 138
135 139 def visit_column(self,column):
136 140 # For SQLite, we *have* to remove the column here so the table
137 141 # is re-created properly.
138 142 column.remove_from_table(column.table,unset_table=False)
139 143 super(SQLiteColumnDropper,self).visit_column(column)
140 144
141 145
142 146 class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
143 147 """SQLite SchemaChanger"""
144 148
145 149 def _modify_table(self, table, column, delta):
146 150 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
147 151
148 152 def visit_index(self, index):
149 153 """Does not support ALTER INDEX"""
150 154 self._not_supported('ALTER INDEX')
151 155
152 156
153 157 class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon):
154 158
155 159 def visit_migrate_primary_key_constraint(self, constraint):
156 160 tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )"
157 161 cols = ', '.join(map(self.preparer.format_column, constraint.columns))
158 162 tname = self.preparer.format_table(constraint.table)
159 163 name = self.get_constraint_name(constraint)
160 164 msg = tmpl % (name, tname, cols)
161 165 self.append(msg)
162 166 self.execute()
163 167
164 168 def _modify_table(self, table, column, delta):
165 169 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
166 170
167 171 def visit_migrate_foreign_key_constraint(self, *p, **k):
168 172 self.recreate_table(p[0].table)
169 173
170 174 def visit_migrate_unique_constraint(self, *p, **k):
171 175 self.recreate_table(p[0].table)
172 176
173 177
174 178 class SQLiteConstraintDropper(ansisql.ANSIColumnDropper,
175 179 SQLiteHelper,
176 180 ansisql.ANSIConstraintCommon):
177 181
178 182 def _modify_table(self, table, column, delta):
179 183 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
180 184
181 185 def visit_migrate_primary_key_constraint(self, constraint):
182 186 tmpl = "DROP INDEX %s "
183 187 name = self.get_constraint_name(constraint)
184 188 msg = tmpl % (name)
185 189 self.append(msg)
186 190 self.execute()
187 191
188 192 def visit_migrate_foreign_key_constraint(self, *p, **k):
189 193 self._not_supported('ALTER TABLE DROP CONSTRAINT')
190 194
191 195 def visit_migrate_check_constraint(self, *p, **k):
192 196 self._not_supported('ALTER TABLE DROP CONSTRAINT')
193 197
194 198 def visit_migrate_unique_constraint(self, *p, **k):
195 199 self.recreate_table(p[0].table, omit_uniques=[p[0].name])
196 200
197 201
198 202 # TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index
199 203
200 204 class SQLiteDialect(ansisql.ANSIDialect):
201 205 columngenerator = SQLiteColumnGenerator
202 206 columndropper = SQLiteColumnDropper
203 207 schemachanger = SQLiteSchemaChanger
204 208 constraintgenerator = SQLiteConstraintGenerator
205 209 constraintdropper = SQLiteConstraintDropper
@@ -1,666 +1,669 b''
1 1 """
2 2 Schema module providing common schema operations.
3 3 """
4 import abc
5 try: # Python 3
6 from collections.abc import MutableMapping as DictMixin
7 except ImportError: # Python 2
8 from UserDict import DictMixin
4 9 import warnings
5 10
6 from UserDict import DictMixin
7
8 11 import sqlalchemy
9 12
10 13 from sqlalchemy.schema import ForeignKeyConstraint
11 14 from sqlalchemy.schema import UniqueConstraint
12 15 from pyramid import compat
13 16
14 17 from rhodecode.lib.dbmigrate.migrate.exceptions import *
15 18 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07, SQLA_08
16 19 from rhodecode.lib.dbmigrate.migrate.changeset import util
17 20 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (
18 21 get_engine_visitor, run_single_visitor)
19 22
20 23
21 24 __all__ = [
22 25 'create_column',
23 26 'drop_column',
24 27 'alter_column',
25 28 'rename_table',
26 29 'rename_index',
27 30 'ChangesetTable',
28 31 'ChangesetColumn',
29 32 'ChangesetIndex',
30 33 'ChangesetDefaultClause',
31 34 'ColumnDelta',
32 35 ]
33 36
34 37 def create_column(column, table=None, *p, **kw):
35 38 """Create a column, given the table.
36 39
37 40 API to :meth:`ChangesetColumn.create`.
38 41 """
39 42 if table is not None:
40 43 return table.create_column(column, *p, **kw)
41 44 return column.create(*p, **kw)
42 45
43 46
44 47 def drop_column(column, table=None, *p, **kw):
45 48 """Drop a column, given the table.
46 49
47 50 API to :meth:`ChangesetColumn.drop`.
48 51 """
49 52 if table is not None:
50 53 return table.drop_column(column, *p, **kw)
51 54 return column.drop(*p, **kw)
52 55
53 56
54 57 def rename_table(table, name, engine=None, **kw):
55 58 """Rename a table.
56 59
57 60 If Table instance is given, engine is not used.
58 61
59 62 API to :meth:`ChangesetTable.rename`.
60 63
61 64 :param table: Table to be renamed.
62 65 :param name: New name for Table.
63 66 :param engine: Engine instance.
64 67 :type table: string or Table instance
65 68 :type name: string
66 69 :type engine: obj
67 70 """
68 71 table = _to_table(table, engine)
69 72 table.rename(name, **kw)
70 73
71 74
72 75 def rename_index(index, name, table=None, engine=None, **kw):
73 76 """Rename an index.
74 77
75 78 If Index instance is given,
76 79 table and engine are not used.
77 80
78 81 API to :meth:`ChangesetIndex.rename`.
79 82
80 83 :param index: Index to be renamed.
81 84 :param name: New name for index.
82 85 :param table: Table to which Index is reffered.
83 86 :param engine: Engine instance.
84 87 :type index: string or Index instance
85 88 :type name: string
86 89 :type table: string or Table instance
87 90 :type engine: obj
88 91 """
89 92 index = _to_index(index, table, engine)
90 93 index.rename(name, **kw)
91 94
92 95
93 96 def alter_column(*p, **k):
94 97 """Alter a column.
95 98
96 99 This is a helper function that creates a :class:`ColumnDelta` and
97 100 runs it.
98 101
99 102 :argument column:
100 103 The name of the column to be altered or a
101 104 :class:`ChangesetColumn` column representing it.
102 105
103 106 :param table:
104 107 A :class:`~sqlalchemy.schema.Table` or table name to
105 108 for the table where the column will be changed.
106 109
107 110 :param engine:
108 111 The :class:`~sqlalchemy.engine.base.Engine` to use for table
109 112 reflection and schema alterations.
110 113
111 114 :returns: A :class:`ColumnDelta` instance representing the change.
112 115
113 116
114 117 """
115 118
116 119 if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
117 120 k['table'] = p[0].table
118 121 if 'engine' not in k:
119 122 k['engine'] = k['table'].bind
120 123
121 124 # deprecation
122 125 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
123 126 warnings.warn(
124 127 "Passing a Column object to alter_column is deprecated."
125 128 " Just pass in keyword parameters instead.",
126 129 MigrateDeprecationWarning
127 130 )
128 131 engine = k['engine']
129 132
130 133 # enough tests seem to break when metadata is always altered
131 134 # that this crutch has to be left in until they can be sorted
132 135 # out
133 136 k['alter_metadata']=True
134 137
135 138 delta = ColumnDelta(*p, **k)
136 139
137 140 visitorcallable = get_engine_visitor(engine, 'schemachanger')
138 141 engine._run_visitor(visitorcallable, delta)
139 142
140 143 return delta
141 144
142 145
143 146 def _to_table(table, engine=None):
144 147 """Return if instance of Table, else construct new with metadata"""
145 148 if isinstance(table, sqlalchemy.Table):
146 149 return table
147 150
148 151 # Given: table name, maybe an engine
149 152 meta = sqlalchemy.MetaData()
150 153 if engine is not None:
151 154 meta.bind = engine
152 155 return sqlalchemy.Table(table, meta)
153 156
154 157
155 158 def _to_index(index, table=None, engine=None):
156 159 """Return if instance of Index, else construct new with metadata"""
157 160 if isinstance(index, sqlalchemy.Index):
158 161 return index
159 162
160 163 # Given: index name; table name required
161 164 table = _to_table(table, engine)
162 165 ret = sqlalchemy.Index(index)
163 166 ret.table = table
164 167 return ret
165 168
166 169
167 170 class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
168 171 """Extracts the differences between two columns/column-parameters
169 172
170 173 May receive parameters arranged in several different ways:
171 174
172 175 * **current_column, new_column, \*p, \*\*kw**
173 176 Additional parameters can be specified to override column
174 177 differences.
175 178
176 179 * **current_column, \*p, \*\*kw**
177 180 Additional parameters alter current_column. Table name is extracted
178 181 from current_column object.
179 182 Name is changed to current_column.name from current_name,
180 183 if current_name is specified.
181 184
182 185 * **current_col_name, \*p, \*\*kw**
183 186 Table kw must specified.
184 187
185 188 :param table: Table at which current Column should be bound to.\
186 189 If table name is given, reflection will be used.
187 190 :type table: string or Table instance
188 191
189 192 :param metadata: A :class:`MetaData` instance to store
190 193 reflected table names
191 194
192 195 :param engine: When reflecting tables, either engine or metadata must \
193 196 be specified to acquire engine object.
194 197 :type engine: :class:`Engine` instance
195 198 :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
196 199 `result_column` through :func:`dict` alike object.
197 200
198 201 * :class:`ColumnDelta`.result_column is altered column with new attributes
199 202
200 203 * :class:`ColumnDelta`.current_name is current name of column in db
201 204
202 205
203 206 """
204 207
205 208 # Column attributes that can be altered
206 209 diff_keys = ('name', 'type', 'primary_key', 'nullable',
207 210 'server_onupdate', 'server_default', 'autoincrement')
208 211 diffs = dict()
209 212 __visit_name__ = 'column'
210 213
211 214 def __init__(self, *p, **kw):
212 215 # 'alter_metadata' is not a public api. It exists purely
213 216 # as a crutch until the tests that fail when 'alter_metadata'
214 217 # behaviour always happens can be sorted out
215 218 self.alter_metadata = kw.pop("alter_metadata", False)
216 219
217 220 self.meta = kw.pop("metadata", None)
218 221 self.engine = kw.pop("engine", None)
219 222
220 223 # Things are initialized differently depending on how many column
221 224 # parameters are given. Figure out how many and call the appropriate
222 225 # method.
223 226 if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
224 227 # At least one column specified
225 228 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
226 229 # Two columns specified
227 230 diffs = self.compare_2_columns(*p, **kw)
228 231 else:
229 232 # Exactly one column specified
230 233 diffs = self.compare_1_column(*p, **kw)
231 234 else:
232 235 # Zero columns specified
233 236 if not len(p) or not isinstance(p[0], compat.string_types):
234 237 raise ValueError("First argument must be column name")
235 238 diffs = self.compare_parameters(*p, **kw)
236 239
237 240 self.apply_diffs(diffs)
238 241
239 242 def __repr__(self):
240 243 return '<ColumnDelta altermetadata=%r, %s>' % (
241 244 self.alter_metadata,
242 245 super(ColumnDelta, self).__repr__()
243 246 )
244 247
245 248 def __getitem__(self, key):
246 249 if key not in self.keys():
247 250 raise KeyError("No such diff key, available: %s" % self.diffs )
248 251 return getattr(self.result_column, key)
249 252
250 253 def __setitem__(self, key, value):
251 254 if key not in self.keys():
252 255 raise KeyError("No such diff key, available: %s" % self.diffs )
253 256 setattr(self.result_column, key, value)
254 257
255 258 def __delitem__(self, key):
256 259 raise NotImplementedError
257 260
258 261 def __len__(self):
259 262 raise NotImplementedError
260 263
261 264 def __iter__(self):
262 265 raise NotImplementedError
263 266
264 267 def keys(self):
265 268 return self.diffs.keys()
266 269
267 270 def compare_parameters(self, current_name, *p, **k):
268 271 """Compares Column objects with reflection"""
269 272 self.table = k.pop('table')
270 273 self.result_column = self._table.c.get(current_name)
271 274 if len(p):
272 275 k = self._extract_parameters(p, k, self.result_column)
273 276 return k
274 277
275 278 def compare_1_column(self, col, *p, **k):
276 279 """Compares one Column object"""
277 280 self.table = k.pop('table', None)
278 281 if self.table is None:
279 282 self.table = col.table
280 283 self.result_column = col
281 284 if len(p):
282 285 k = self._extract_parameters(p, k, self.result_column)
283 286 return k
284 287
285 288 def compare_2_columns(self, old_col, new_col, *p, **k):
286 289 """Compares two Column objects"""
287 290 self.process_column(new_col)
288 291 self.table = k.pop('table', None)
289 292 # we cannot use bool() on table in SA06
290 293 if self.table is None:
291 294 self.table = old_col.table
292 295 if self.table is None:
293 296 new_col.table
294 297 self.result_column = old_col
295 298
296 299 # set differences
297 300 # leave out some stuff for later comp
298 301 for key in (set(self.diff_keys) - set(('type',))):
299 302 val = getattr(new_col, key, None)
300 303 if getattr(self.result_column, key, None) != val:
301 304 k.setdefault(key, val)
302 305
303 306 # inspect types
304 307 if not self.are_column_types_eq(self.result_column.type, new_col.type):
305 308 k.setdefault('type', new_col.type)
306 309
307 310 if len(p):
308 311 k = self._extract_parameters(p, k, self.result_column)
309 312 return k
310 313
311 314 def apply_diffs(self, diffs):
312 315 """Populate dict and column object with new values"""
313 316 self.diffs = diffs
314 317 for key in self.diff_keys:
315 318 if key in diffs:
316 319 setattr(self.result_column, key, diffs[key])
317 320
318 321 self.process_column(self.result_column)
319 322
320 323 # create an instance of class type if not yet
321 324 if 'type' in diffs and callable(self.result_column.type):
322 325 self.result_column.type = self.result_column.type()
323 326
324 327 # add column to the table
325 328 if self.table is not None and self.alter_metadata:
326 329 self.result_column.add_to_table(self.table)
327 330
328 331 def are_column_types_eq(self, old_type, new_type):
329 332 """Compares two types to be equal"""
330 333 ret = old_type.__class__ == new_type.__class__
331 334
332 335 # String length is a special case
333 336 if ret and isinstance(new_type, sqlalchemy.types.String):
334 337 ret = (getattr(old_type, 'length', None) == \
335 338 getattr(new_type, 'length', None))
336 339 return ret
337 340
338 341 def _extract_parameters(self, p, k, column):
339 342 """Extracts data from p and modifies diffs"""
340 343 p = list(p)
341 344 while len(p):
342 345 if isinstance(p[0], compat.string_types):
343 346 k.setdefault('name', p.pop(0))
344 347 elif isinstance(p[0], sqlalchemy.types.TypeEngine):
345 348 k.setdefault('type', p.pop(0))
346 349 elif callable(p[0]):
347 350 p[0] = p[0]()
348 351 else:
349 352 break
350 353
351 354 if len(p):
352 355 new_col = column.copy_fixed()
353 356 new_col._init_items(*p)
354 357 k = self.compare_2_columns(column, new_col, **k)
355 358 return k
356 359
357 360 def process_column(self, column):
358 361 """Processes default values for column"""
359 362 # XXX: this is a snippet from SA processing of positional parameters
360 363 toinit = list()
361 364
362 365 if column.server_default is not None:
363 366 if isinstance(column.server_default, sqlalchemy.FetchedValue):
364 367 toinit.append(column.server_default)
365 368 else:
366 369 toinit.append(sqlalchemy.DefaultClause(column.server_default))
367 370 if column.server_onupdate is not None:
368 371 if isinstance(column.server_onupdate, FetchedValue):
369 372 toinit.append(column.server_default)
370 373 else:
371 374 toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
372 375 for_update=True))
373 376 if toinit:
374 377 column._init_items(*toinit)
375 378
376 379 def _get_table(self):
377 380 return getattr(self, '_table', None)
378 381
379 382 def _set_table(self, table):
380 383 if isinstance(table, compat.string_types):
381 384 if self.alter_metadata:
382 385 if not self.meta:
383 386 raise ValueError("metadata must be specified for table"
384 387 " reflection when using alter_metadata")
385 388 meta = self.meta
386 389 if self.engine:
387 390 meta.bind = self.engine
388 391 else:
389 392 if not self.engine and not self.meta:
390 393 raise ValueError("engine or metadata must be specified"
391 394 " to reflect tables")
392 395 if not self.engine:
393 396 self.engine = self.meta.bind
394 397 meta = sqlalchemy.MetaData(bind=self.engine)
395 398 self._table = sqlalchemy.Table(table, meta, autoload=True)
396 399 elif isinstance(table, sqlalchemy.Table):
397 400 self._table = table
398 401 if not self.alter_metadata:
399 402 self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
400 403 def _get_result_column(self):
401 404 return getattr(self, '_result_column', None)
402 405
403 406 def _set_result_column(self, column):
404 407 """Set Column to Table based on alter_metadata evaluation."""
405 408 self.process_column(column)
406 409 if not hasattr(self, 'current_name'):
407 410 self.current_name = column.name
408 411 if self.alter_metadata:
409 412 self._result_column = column
410 413 else:
411 414 self._result_column = column.copy_fixed()
412 415
413 416 table = property(_get_table, _set_table)
414 417 result_column = property(_get_result_column, _set_result_column)
415 418
416 419
417 420 class ChangesetTable(object):
418 421 """Changeset extensions to SQLAlchemy tables."""
419 422
420 423 def create_column(self, column, *p, **kw):
421 424 """Creates a column.
422 425
423 426 The column parameter may be a column definition or the name of
424 427 a column in this table.
425 428
426 429 API to :meth:`ChangesetColumn.create`
427 430
428 431 :param column: Column to be created
429 432 :type column: Column instance or string
430 433 """
431 434 if not isinstance(column, sqlalchemy.Column):
432 435 # It's a column name
433 436 column = getattr(self.c, str(column))
434 437 column.create(table=self, *p, **kw)
435 438
436 439 def drop_column(self, column, *p, **kw):
437 440 """Drop a column, given its name or definition.
438 441
439 442 API to :meth:`ChangesetColumn.drop`
440 443
441 444 :param column: Column to be droped
442 445 :type column: Column instance or string
443 446 """
444 447 if not isinstance(column, sqlalchemy.Column):
445 448 # It's a column name
446 449 try:
447 450 column = getattr(self.c, str(column))
448 451 except AttributeError:
449 452 # That column isn't part of the table. We don't need
450 453 # its entire definition to drop the column, just its
451 454 # name, so create a dummy column with the same name.
452 455 column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
453 456 column.drop(table=self, *p, **kw)
454 457
455 458 def rename(self, name, connection=None, **kwargs):
456 459 """Rename this table.
457 460
458 461 :param name: New name of the table.
459 462 :type name: string
460 463 :param connection: reuse connection istead of creating new one.
461 464 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
462 465 """
463 466 engine = self.bind
464 467 self.new_name = name
465 468 visitorcallable = get_engine_visitor(engine, 'schemachanger')
466 469 run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
467 470
468 471 # Fix metadata registration
469 472 self.name = name
470 473 self.deregister()
471 474 self._set_parent(self.metadata)
472 475
473 476 def _meta_key(self):
474 477 """Get the meta key for this table."""
475 478 return sqlalchemy.schema._get_table_key(self.name, self.schema)
476 479
477 480 def deregister(self):
478 481 """Remove this table from its metadata"""
479 482 if SQLA_07:
480 483 self.metadata._remove_table(self.name, self.schema)
481 484 else:
482 485 key = self._meta_key()
483 486 meta = self.metadata
484 487 if key in meta.tables:
485 488 del meta.tables[key]
486 489
487 490
488 491 class ChangesetColumn(object):
489 492 """Changeset extensions to SQLAlchemy columns."""
490 493
491 494 def alter(self, *p, **k):
492 495 """Makes a call to :func:`alter_column` for the column this
493 496 method is called on.
494 497 """
495 498 if 'table' not in k:
496 499 k['table'] = self.table
497 500 if 'engine' not in k:
498 501 k['engine'] = k['table'].bind
499 502 return alter_column(self, *p, **k)
500 503
501 504 def create(self, table=None, index_name=None, unique_name=None,
502 505 primary_key_name=None, populate_default=True, connection=None, **kwargs):
503 506 """Create this column in the database.
504 507
505 508 Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
506 509 for most databases.
507 510
508 511 :param table: Table instance to create on.
509 512 :param index_name: Creates :class:`ChangesetIndex` on this column.
510 513 :param unique_name: Creates :class:\
511 514 `~migrate.changeset.constraint.UniqueConstraint` on this column.
512 515 :param primary_key_name: Creates :class:\
513 516 `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column.
514 517 :param populate_default: If True, created column will be \
515 518 populated with defaults
516 519 :param connection: reuse connection istead of creating new one.
517 520 :type table: Table instance
518 521 :type index_name: string
519 522 :type unique_name: string
520 523 :type primary_key_name: string
521 524 :type populate_default: bool
522 525 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
523 526
524 527 :returns: self
525 528 """
526 529 self.populate_default = populate_default
527 530 self.index_name = index_name
528 531 self.unique_name = unique_name
529 532 self.primary_key_name = primary_key_name
530 533 for cons in ('index_name', 'unique_name', 'primary_key_name'):
531 534 self._check_sanity_constraints(cons)
532 535
533 536 self.add_to_table(table)
534 537 engine = self.table.bind
535 538 visitorcallable = get_engine_visitor(engine, 'columngenerator')
536 539 engine._run_visitor(visitorcallable, self, connection, **kwargs)
537 540
538 541 # TODO: reuse existing connection
539 542 if self.populate_default and self.default is not None:
540 543 stmt = table.update().values({self: engine._execute_default(self.default)})
541 544 engine.execute(stmt)
542 545
543 546 return self
544 547
545 548 def drop(self, table=None, connection=None, **kwargs):
546 549 """Drop this column from the database, leaving its table intact.
547 550
548 551 ``ALTER TABLE DROP COLUMN``, for most databases.
549 552
550 553 :param connection: reuse connection istead of creating new one.
551 554 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
552 555 """
553 556 if table is not None:
554 557 self.table = table
555 558 engine = self.table.bind
556 559 visitorcallable = get_engine_visitor(engine, 'columndropper')
557 560 engine._run_visitor(visitorcallable, self, connection, **kwargs)
558 561 self.remove_from_table(self.table, unset_table=False)
559 562 self.table = None
560 563 return self
561 564
562 565 def add_to_table(self, table):
563 566 if table is not None and self.table is None:
564 567 if SQLA_07:
565 568 table.append_column(self)
566 569 else:
567 570 self._set_parent(table)
568 571
569 572 def _col_name_in_constraint(self,cons,name):
570 573 return False
571 574
572 575 def remove_from_table(self, table, unset_table=True):
573 576 # TODO: remove primary keys, constraints, etc
574 577 if unset_table:
575 578 self.table = None
576 579
577 580 to_drop = set()
578 581 for index in table.indexes:
579 582 columns = []
580 583 for col in index.columns:
581 584 if col.name!=self.name:
582 585 columns.append(col)
583 586 if columns:
584 587 index.columns = columns
585 588 if SQLA_08:
586 589 index.expressions = columns
587 590 else:
588 591 to_drop.add(index)
589 592 table.indexes = table.indexes - to_drop
590 593
591 594 to_drop = set()
592 595 for cons in table.constraints:
593 596 # TODO: deal with other types of constraint
594 597 if isinstance(cons,(ForeignKeyConstraint,
595 598 UniqueConstraint)):
596 599 for col_name in cons.columns:
597 600 if not isinstance(col_name, compat.string_types):
598 601 col_name = col_name.name
599 602 if self.name==col_name:
600 603 to_drop.add(cons)
601 604 table.constraints = table.constraints - to_drop
602 605
603 606 if table.c.contains_column(self):
604 607 if SQLA_07:
605 608 table._columns.remove(self)
606 609 else:
607 610 table.c.remove(self)
608 611
609 612 # TODO: this is fixed in 0.6
610 613 def copy_fixed(self, **kw):
611 614 """Create a copy of this ``Column``, with all attributes."""
612 615 q = util.safe_quote(self)
613 616 return sqlalchemy.Column(self.name, self.type, self.default,
614 617 key=self.key,
615 618 primary_key=self.primary_key,
616 619 nullable=self.nullable,
617 620 quote=q,
618 621 index=self.index,
619 622 unique=self.unique,
620 623 onupdate=self.onupdate,
621 624 autoincrement=self.autoincrement,
622 625 server_default=self.server_default,
623 626 server_onupdate=self.server_onupdate,
624 627 *[c.copy(**kw) for c in self.constraints])
625 628
626 629 def _check_sanity_constraints(self, name):
627 630 """Check if constraints names are correct"""
628 631 obj = getattr(self, name)
629 632 if (getattr(self, name[:-5]) and not obj):
630 633 raise InvalidConstraintError("Column.create() accepts index_name,"
631 634 " primary_key_name and unique_name to generate constraints")
632 635 if not isinstance(obj, compat.string_types) and obj is not None:
633 636 raise InvalidConstraintError(
634 637 "%s argument for column must be constraint name" % name)
635 638
636 639
637 640 class ChangesetIndex(object):
638 641 """Changeset extensions to SQLAlchemy Indexes."""
639 642
640 643 __visit_name__ = 'index'
641 644
642 645 def rename(self, name, connection=None, **kwargs):
643 646 """Change the name of an index.
644 647
645 648 :param name: New name of the Index.
646 649 :type name: string
647 650 :param connection: reuse connection istead of creating new one.
648 651 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
649 652 """
650 653 engine = self.table.bind
651 654 self.new_name = name
652 655 visitorcallable = get_engine_visitor(engine, 'schemachanger')
653 656 engine._run_visitor(visitorcallable, self, connection, **kwargs)
654 657 self.name = name
655 658
656 659
657 660 class ChangesetDefaultClause(object):
658 661 """Implements comparison between :class:`DefaultClause` instances"""
659 662
660 663 def __eq__(self, other):
661 664 if isinstance(other, self.__class__):
662 665 if self.arg == other.arg:
663 666 return True
664 667
665 668 def __ne__(self, other):
666 669 return not self.__eq__(other)
@@ -1,10 +1,21 b''
1 1 """
2 2 Safe quoting method
3 3 """
4 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_10
5
6
7 def fk_column_names(constraint):
8 if SQLA_10:
9 return [
10 constraint.columns[key].name for key in constraint.column_keys]
11 else:
12 return [
13 element.parent.name for element in constraint.elements]
14
4 15
5 16 def safe_quote(obj):
6 17 # this is the SQLA 0.9 approach
7 18 if hasattr(obj, 'name') and hasattr(obj.name, 'quote'):
8 19 return obj.name.quote
9 20 else:
10 21 return obj.quote
@@ -1,87 +1,91 b''
1 1 """
2 2 Provide exception classes for :mod:`migrate`
3 3 """
4 4
5 5
6 6 class Error(Exception):
7 7 """Error base class."""
8 8
9 9
10 10 class ApiError(Error):
11 11 """Base class for API errors."""
12 12
13 13
14 14 class KnownError(ApiError):
15 15 """A known error condition."""
16 16
17 17
18 18 class UsageError(ApiError):
19 19 """A known error condition where help should be displayed."""
20 20
21 21
22 22 class ControlledSchemaError(Error):
23 23 """Base class for controlled schema errors."""
24 24
25 25
26 26 class InvalidVersionError(ControlledSchemaError):
27 27 """Invalid version number."""
28 28
29 29
30 class VersionNotFoundError(KeyError):
31 """Specified version is not present."""
32
33
30 34 class DatabaseNotControlledError(ControlledSchemaError):
31 35 """Database should be under version control, but it's not."""
32 36
33 37
34 38 class DatabaseAlreadyControlledError(ControlledSchemaError):
35 39 """Database shouldn't be under version control, but it is"""
36 40
37 41
38 42 class WrongRepositoryError(ControlledSchemaError):
39 43 """This database is under version control by another repository."""
40 44
41 45
42 46 class NoSuchTableError(ControlledSchemaError):
43 47 """The table does not exist."""
44 48
45 49
46 50 class PathError(Error):
47 51 """Base class for path errors."""
48 52
49 53
50 54 class PathNotFoundError(PathError):
51 55 """A path with no file was required; found a file."""
52 56
53 57
54 58 class PathFoundError(PathError):
55 59 """A path with a file was required; found no file."""
56 60
57 61
58 62 class RepositoryError(Error):
59 63 """Base class for repository errors."""
60 64
61 65
62 66 class InvalidRepositoryError(RepositoryError):
63 67 """Invalid repository error."""
64 68
65 69
66 70 class ScriptError(Error):
67 71 """Base class for script errors."""
68 72
69 73
70 74 class InvalidScriptError(ScriptError):
71 75 """Invalid script error."""
72 76
73 77
74 78 class InvalidVersionError(Error):
75 79 """Invalid version error."""
76 80
77 81 # migrate.changeset
78 82
79 83 class NotSupportedError(Error):
80 84 """Not supported error"""
81 85
82 86
83 87 class InvalidConstraintError(Error):
84 88 """Invalid constraint error"""
85 89
86 90 class MigrateDeprecationWarning(DeprecationWarning):
87 91 """Warning for deprecated features in Migrate"""
General Comments 0
You need to be logged in to leave comments. Login now