##// END OF EJS Templates
dbmigrate: sync with latest ver
super-admin -
r5165:e6085588 default
parent child Browse files
Show More
@@ -1,209 +1,230 b''
1 """
1 """
2 `SQLite`_ database specific implementations of changeset classes.
2 `SQLite`_ database specific implementations of changeset classes.
3
3
4 .. _`SQLite`: http://www.sqlite.org/
4 .. _`SQLite`: http://www.sqlite.org/
5 """
5 """
6 try: # Python 3
6 try: # Python 3
7 from collections.abc import MutableMapping as DictMixin
7 from collections.abc import MutableMapping as DictMixin
8 except ImportError: # Python 2
8 except ImportError: # Python 2
9 from UserDict import DictMixin
9 from UserDict import DictMixin
10 from copy import copy
10 from copy import copy
11 import re
11 import re
12
12
13 from sqlalchemy.databases import sqlite as sa_base
13 from sqlalchemy.databases import sqlite as sa_base
14 from sqlalchemy.schema import ForeignKeyConstraint
14 from sqlalchemy.schema import ForeignKeyConstraint
15 from sqlalchemy.schema import UniqueConstraint
15 from sqlalchemy.schema import UniqueConstraint
16
16
17 from rhodecode.lib.dbmigrate.migrate import exceptions
17 from rhodecode.lib.dbmigrate.migrate import exceptions
18 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql
18 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql
19 import sqlite3
19 import sqlite3
20
20
21 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
21 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
22
22
23
23
24 class SQLiteCommon(object):
24 class SQLiteCommon(object):
25
25
26 def _not_supported(self, op):
26 def _not_supported(self, op):
27 raise exceptions.NotSupportedError("SQLite does not support "
27 raise exceptions.NotSupportedError("SQLite does not support "
28 "%s; see http://www.sqlite.org/lang_altertable.html" % op)
28 "%s; see http://www.sqlite.org/lang_altertable.html" % op)
29
29
30
30
31 class SQLiteHelper(SQLiteCommon):
31 class SQLiteHelper(SQLiteCommon):
32
32
33 def _get_unique_constraints(self, table):
33 def _filter_columns(self, cols, table):
34 """Retrieve information about existing unique constraints of the table
34 """Splits the string of columns and returns those only in the table.
35
36 :param cols: comma-delimited string of table columns
37 :param table: the table to check
38 :return: list of columns in the table
39 """
40 columns = []
41 for c in cols.split(","):
42 if c in table.columns:
43 # There was a bug in reflection of SQLite columns with
44 # reserved identifiers as names (SQLite can return them
45 # wrapped with double quotes), so strip double quotes.
46 columns.extend(c.strip(' "'))
47 return columns
48
49 def _get_constraints(self, table):
50 """Retrieve information about existing constraints of the table
35
51
36 This feature is needed for recreate_table() to work properly.
52 This feature is needed for recreate_table() to work properly.
37 """
53 """
38
54
39 data = table.metadata.bind.execute(
55 data = table.metadata.bind.execute(
40 """SELECT sql
56 """SELECT sql
41 FROM sqlite_master
57 FROM sqlite_master
42 WHERE
58 WHERE
43 type='table' AND
59 type='table' AND
44 name=:table_name""",
60 name=:table_name""",
45 table_name=table.name
61 table_name=table.name
46 ).fetchone()[0]
62 ).fetchone()[0]
47
63
48 UNIQUE_PATTERN = "CONSTRAINT (\w+) UNIQUE \(([^\)]+)\)"
64 UNIQUE_PATTERN = "CONSTRAINT (\w+) UNIQUE \(([^\)]+)\)"
49 constraints = []
65 constraints = []
50 for name, cols in re.findall(UNIQUE_PATTERN, data):
66 for name, cols in re.findall(UNIQUE_PATTERN, data):
51 # Filter out any columns that were dropped from the table.
67 # Filter out any columns that were dropped from the table.
52 columns = []
68 columns = self._filter_columns(cols, table)
53 for c in cols.split(","):
54 if c in table.columns:
55 # There was a bug in reflection of SQLite columns with
56 # reserved identifiers as names (SQLite can return them
57 # wrapped with double quotes), so strip double quotes.
58 columns.extend(c.strip(' "'))
59 if columns:
69 if columns:
60 constraints.extend(UniqueConstraint(*columns, name=name))
70 constraints.extend(UniqueConstraint(*columns, name=name))
71
72 FKEY_PATTERN = "CONSTRAINT (\w+) FOREIGN KEY \(([^\)]+)\)"
73 for name, cols in re.findall(FKEY_PATTERN, data):
74 # Filter out any columns that were dropped from the table.
75 columns = self._filter_columns(cols, table)
76 if columns:
77 constraints.extend(ForeignKeyConstraint(*columns, name=name))
78
61 return constraints
79 return constraints
62
80
63 def recreate_table(self, table, column=None, delta=None,
81 def recreate_table(self, table, column=None, delta=None,
64 omit_uniques=None):
82 omit_constraints=None):
65 table_name = self.preparer.format_table(table)
83 table_name = self.preparer.format_table(table)
66
84
67 # we remove all indexes so as not to have
85 # we remove all indexes so as not to have
68 # problems during copy and re-create
86 # problems during copy and re-create
69 for index in table.indexes:
87 for index in table.indexes:
70 index.drop()
88 index.drop()
71
89
72 # reflect existing unique constraints
90 # reflect existing constraints
73 for uc in self._get_unique_constraints(table):
91 for constraint in self._get_constraints(table):
74 table.append_constraint(uc)
92 table.append_constraint(constraint)
75 # omit given unique constraints when creating a new table if required
93 # omit given constraints when creating a new table if required
76 table.constraints = set([
94 table.constraints = set([
77 cons for cons in table.constraints
95 cons for cons in table.constraints
78 if omit_uniques is None or cons.name not in omit_uniques
96 if omit_constraints is None or cons.name not in omit_constraints
79 ])
97 ])
80 tup = sqlite3.sqlite_version_info
98
81 if tup[0] > 3 or (tup[0] == 3 and tup[1] >= 26):
99 # Use "PRAGMA legacy_alter_table = ON" with sqlite >= 3.26 when
100 # using "ALTER TABLE RENAME TO migration_tmp" to maintain legacy
101 # behavior. See: https://www.sqlite.org/src/info/ae9638e9c0ad0c36
102 if self.connection.engine.dialect.server_version_info >= (3, 26):
82 self.append('PRAGMA legacy_alter_table = ON')
103 self.append('PRAGMA legacy_alter_table = ON')
83 self.execute()
104 self.execute()
84
85 self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
105 self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
86 self.execute()
106 self.execute()
87 if tup[0] > 3 or (tup[0] == 3 and tup[1] >= 26):
107 if self.connection.engine.dialect.server_version_info >= (3, 26):
88 self.append('PRAGMA legacy_alter_table = OFF')
108 self.append('PRAGMA legacy_alter_table = OFF')
89 self.execute()
109 self.execute()
110
90 insertion_string = self._modify_table(table, column, delta)
111 insertion_string = self._modify_table(table, column, delta)
91
112
92 table.create(bind=self.connection)
113 table.create(bind=self.connection)
93 self.append(insertion_string % {'table_name': table_name})
114 self.append(insertion_string % {'table_name': table_name})
94 self.execute()
115 self.execute()
95 self.append('DROP TABLE migration_tmp')
116 self.append('DROP TABLE migration_tmp')
96 self.execute()
117 self.execute()
97
118
98 def visit_column(self, delta):
119 def visit_column(self, delta):
99 if isinstance(delta, DictMixin):
120 if isinstance(delta, DictMixin):
100 column = delta.result_column
121 column = delta.result_column
101 table = self._to_table(delta.table)
122 table = self._to_table(delta.table)
102 else:
123 else:
103 column = delta
124 column = delta
104 table = self._to_table(column.table)
125 table = self._to_table(column.table)
105
106 self.recreate_table(table,column,delta)
126 self.recreate_table(table,column,delta)
107
127
108 class SQLiteColumnGenerator(SQLiteSchemaGenerator,
128 class SQLiteColumnGenerator(SQLiteSchemaGenerator,
109 ansisql.ANSIColumnGenerator,
129 ansisql.ANSIColumnGenerator,
110 # at the end so we get the normal
130 # at the end so we get the normal
111 # visit_column by default
131 # visit_column by default
112 SQLiteHelper,
132 SQLiteHelper,
113 SQLiteCommon
133 SQLiteCommon
114 ):
134 ):
115 """SQLite ColumnGenerator"""
135 """SQLite ColumnGenerator"""
116
136
117 def _modify_table(self, table, column, delta):
137 def _modify_table(self, table, column, delta):
118 columns = ' ,'.join(map(
138 columns = ' ,'.join(map(
119 self.preparer.format_column,
139 self.preparer.format_column,
120 [c for c in table.columns if c.name!=column.name]))
140 [c for c in table.columns if c.name!=column.name]))
121 return ('INSERT INTO %%(table_name)s (%(cols)s) '
141 return ('INSERT INTO %%(table_name)s (%(cols)s) '
122 'SELECT %(cols)s from migration_tmp')%{'cols':columns}
142 'SELECT %(cols)s from migration_tmp')%{'cols':columns}
123
143
124 def visit_column(self,column):
144 def visit_column(self,column):
125 if column.foreign_keys:
145 if column.foreign_keys:
126 SQLiteHelper.visit_column(self,column)
146 SQLiteHelper.visit_column(self,column)
127 else:
147 else:
128 super(SQLiteColumnGenerator,self).visit_column(column)
148 super(SQLiteColumnGenerator,self).visit_column(column)
129
149
130 class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
150 class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
131 """SQLite ColumnDropper"""
151 """SQLite ColumnDropper"""
132
152
133 def _modify_table(self, table, column, delta):
153 def _modify_table(self, table, column, delta):
134
154
135 columns = ' ,'.join(map(self.preparer.format_column, table.columns))
155 columns = ' ,'.join(map(self.preparer.format_column, table.columns))
136 return 'INSERT INTO %(table_name)s SELECT ' + columns + \
156 return 'INSERT INTO %(table_name)s SELECT ' + columns + \
137 ' from migration_tmp'
157 ' from migration_tmp'
138
158
139 def visit_column(self,column):
159 def visit_column(self,column):
140 # For SQLite, we *have* to remove the column here so the table
160 # For SQLite, we *have* to remove the column here so the table
141 # is re-created properly.
161 # is re-created properly.
142 column.remove_from_table(column.table,unset_table=False)
162 column.remove_from_table(column.table,unset_table=False)
143 super(SQLiteColumnDropper,self).visit_column(column)
163 super(SQLiteColumnDropper,self).visit_column(column)
144
164
145
165
146 class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
166 class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
147 """SQLite SchemaChanger"""
167 """SQLite SchemaChanger"""
148
168
149 def _modify_table(self, table, column, delta):
169 def _modify_table(self, table, column, delta):
150 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
170 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
151
171
152 def visit_index(self, index):
172 def visit_index(self, index):
153 """Does not support ALTER INDEX"""
173 """Does not support ALTER INDEX"""
154 self._not_supported('ALTER INDEX')
174 self._not_supported('ALTER INDEX')
155
175
156
176
157 class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon):
177 class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon):
158
178
159 def visit_migrate_primary_key_constraint(self, constraint):
179 def visit_migrate_primary_key_constraint(self, constraint):
160 tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )"
180 tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )"
161 cols = ', '.join(map(self.preparer.format_column, constraint.columns))
181 cols = ', '.join(map(self.preparer.format_column, constraint.columns))
162 tname = self.preparer.format_table(constraint.table)
182 tname = self.preparer.format_table(constraint.table)
163 name = self.get_constraint_name(constraint)
183 name = self.get_constraint_name(constraint)
164 msg = tmpl % (name, tname, cols)
184 msg = tmpl % (name, tname, cols)
165 self.append(msg)
185 self.append(msg)
166 self.execute()
186 self.execute()
167
187
168 def _modify_table(self, table, column, delta):
188 def _modify_table(self, table, column, delta):
169 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
189 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
170
190
171 def visit_migrate_foreign_key_constraint(self, *p, **k):
191 def visit_migrate_foreign_key_constraint(self, *p, **k):
172 self.recreate_table(p[0].table)
192 self.recreate_table(p[0].table)
173
193
174 def visit_migrate_unique_constraint(self, *p, **k):
194 def visit_migrate_unique_constraint(self, *p, **k):
175 self.recreate_table(p[0].table)
195 self.recreate_table(p[0].table)
176
196
177
197
178 class SQLiteConstraintDropper(ansisql.ANSIColumnDropper,
198 class SQLiteConstraintDropper(ansisql.ANSIColumnDropper,
179 SQLiteHelper,
199 SQLiteHelper,
180 ansisql.ANSIConstraintCommon):
200 ansisql.ANSIConstraintCommon):
181
201
182 def _modify_table(self, table, column, delta):
202 def _modify_table(self, table, column, delta):
183 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
203 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
184
204
185 def visit_migrate_primary_key_constraint(self, constraint):
205 def visit_migrate_primary_key_constraint(self, constraint):
186 tmpl = "DROP INDEX %s "
206 tmpl = "DROP INDEX %s "
187 name = self.get_constraint_name(constraint)
207 name = self.get_constraint_name(constraint)
188 msg = tmpl % (name)
208 msg = tmpl % (name)
189 self.append(msg)
209 self.append(msg)
190 self.execute()
210 self.execute()
191
211
192 def visit_migrate_foreign_key_constraint(self, *p, **k):
212 def visit_migrate_foreign_key_constraint(self, *p, **k):
193 self._not_supported('ALTER TABLE DROP CONSTRAINT')
213 #self._not_supported('ALTER TABLE DROP CONSTRAINT')
214 self.recreate_table(p[0].table, omit_constraints=[p[0].name])
194
215
195 def visit_migrate_check_constraint(self, *p, **k):
216 def visit_migrate_check_constraint(self, *p, **k):
196 self._not_supported('ALTER TABLE DROP CONSTRAINT')
217 self._not_supported('ALTER TABLE DROP CONSTRAINT')
197
218
198 def visit_migrate_unique_constraint(self, *p, **k):
219 def visit_migrate_unique_constraint(self, *p, **k):
199 self.recreate_table(p[0].table, omit_uniques=[p[0].name])
220 self.recreate_table(p[0].table, omit_constraints=[p[0].name])
200
221
201
222
202 # TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index
223 # TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index
203
224
204 class SQLiteDialect(ansisql.ANSIDialect):
225 class SQLiteDialect(ansisql.ANSIDialect):
205 columngenerator = SQLiteColumnGenerator
226 columngenerator = SQLiteColumnGenerator
206 columndropper = SQLiteColumnDropper
227 columndropper = SQLiteColumnDropper
207 schemachanger = SQLiteSchemaChanger
228 schemachanger = SQLiteSchemaChanger
208 constraintgenerator = SQLiteConstraintGenerator
229 constraintgenerator = SQLiteConstraintGenerator
209 constraintdropper = SQLiteConstraintDropper
230 constraintdropper = SQLiteConstraintDropper
@@ -1,715 +1,721 b''
1 """
1 """
2 Schema module providing common schema operations.
2 Schema module providing common schema operations.
3 """
3 """
4 import abc
4 import abc
5 try: # Python 3
5 try: # Python 3
6 from collections.abc import MutableMapping as DictMixin
6 from collections.abc import MutableMapping as DictMixin
7 except ImportError: # Python 2
7 except ImportError: # Python 2
8 from UserDict import DictMixin
8 from UserDict import DictMixin
9 import warnings
9 import warnings
10
10
11 import sqlalchemy
11 import sqlalchemy
12
12
13 from sqlalchemy.schema import ForeignKeyConstraint
13 from sqlalchemy.schema import ForeignKeyConstraint
14 from sqlalchemy.schema import UniqueConstraint
14 from sqlalchemy.schema import UniqueConstraint
15
15
16 from rhodecode.lib.dbmigrate.migrate.exceptions import *
16 from rhodecode.lib.dbmigrate.migrate.exceptions import *
17 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07, SQLA_08
17 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07, SQLA_08
18 from rhodecode.lib.dbmigrate.migrate.changeset import util
18 from rhodecode.lib.dbmigrate.migrate.changeset import util
19 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (
19 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (
20 get_engine_visitor, run_single_visitor)
20 get_engine_visitor, run_single_visitor)
21
21
22
22
23 __all__ = [
23 __all__ = [
24 'create_column',
24 'create_column',
25 'drop_column',
25 'drop_column',
26 'alter_column',
26 'alter_column',
27 'rename_table',
27 'rename_table',
28 'rename_index',
28 'rename_index',
29 'ChangesetTable',
29 'ChangesetTable',
30 'ChangesetColumn',
30 'ChangesetColumn',
31 'ChangesetIndex',
31 'ChangesetIndex',
32 'ChangesetDefaultClause',
32 'ChangesetDefaultClause',
33 'ColumnDelta',
33 'ColumnDelta',
34 ]
34 ]
35
35
36
36
37 def create_column(column, table=None, *p, **kw):
37 def create_column(column, table=None, *p, **kw):
38 """Create a column, given the table.
38 """Create a column, given the table.
39
39
40 API to :meth:`ChangesetColumn.create`.
40 API to :meth:`ChangesetColumn.create`.
41 """
41 """
42 if table is not None:
42 if table is not None:
43 return table.create_column(column, *p, **kw)
43 return table.create_column(column, *p, **kw)
44 return column.create(*p, **kw)
44 return column.create(*p, **kw)
45
45
46
46
47 def drop_column(column, table=None, *p, **kw):
47 def drop_column(column, table=None, *p, **kw):
48 """Drop a column, given the table.
48 """Drop a column, given the table.
49
49
50 API to :meth:`ChangesetColumn.drop`.
50 API to :meth:`ChangesetColumn.drop`.
51 """
51 """
52 if table is not None:
52 if table is not None:
53 return table.drop_column(column, *p, **kw)
53 return table.drop_column(column, *p, **kw)
54 return column.drop(*p, **kw)
54 return column.drop(*p, **kw)
55
55
56
56
57 def rename_table(table, name, engine=None, **kw):
57 def rename_table(table, name, engine=None, **kw):
58 """Rename a table.
58 """Rename a table.
59
59
60 If Table instance is given, engine is not used.
60 If Table instance is given, engine is not used.
61
61
62 API to :meth:`ChangesetTable.rename`.
62 API to :meth:`ChangesetTable.rename`.
63
63
64 :param table: Table to be renamed.
64 :param table: Table to be renamed.
65 :param name: New name for Table.
65 :param name: New name for Table.
66 :param engine: Engine instance.
66 :param engine: Engine instance.
67 :type table: string or Table instance
67 :type table: string or Table instance
68 :type name: string
68 :type name: string
69 :type engine: obj
69 :type engine: obj
70 """
70 """
71 table = _to_table(table, engine)
71 table = _to_table(table, engine)
72 table.rename(name, **kw)
72 table.rename(name, **kw)
73
73
74
74
75 def rename_index(index, name, table=None, engine=None, **kw):
75 def rename_index(index, name, table=None, engine=None, **kw):
76 """Rename an index.
76 """Rename an index.
77
77
78 If Index instance is given,
78 If Index instance is given,
79 table and engine are not used.
79 table and engine are not used.
80
80
81 API to :meth:`ChangesetIndex.rename`.
81 API to :meth:`ChangesetIndex.rename`.
82
82
83 :param index: Index to be renamed.
83 :param index: Index to be renamed.
84 :param name: New name for index.
84 :param name: New name for index.
85 :param table: Table to which Index is reffered.
85 :param table: Table to which Index is reffered.
86 :param engine: Engine instance.
86 :param engine: Engine instance.
87 :type index: string or Index instance
87 :type index: string or Index instance
88 :type name: string
88 :type name: string
89 :type table: string or Table instance
89 :type table: string or Table instance
90 :type engine: obj
90 :type engine: obj
91 """
91 """
92 index = _to_index(index, table, engine)
92 index = _to_index(index, table, engine)
93 index.rename(name, **kw)
93 index.rename(name, **kw)
94
94
95
95
96 def alter_column(*p, **k):
96 def alter_column(*p, **k):
97 """Alter a column.
97 """Alter a column.
98
98
99 This is a helper function that creates a :class:`ColumnDelta` and
99 This is a helper function that creates a :class:`ColumnDelta` and
100 runs it.
100 runs it.
101
101
102 :argument column:
102 :argument column:
103 The name of the column to be altered or a
103 The name of the column to be altered or a
104 :class:`ChangesetColumn` column representing it.
104 :class:`ChangesetColumn` column representing it.
105
105
106 :param table:
106 :param table:
107 A :class:`~sqlalchemy.schema.Table` or table name to
107 A :class:`~sqlalchemy.schema.Table` or table name to
108 for the table where the column will be changed.
108 for the table where the column will be changed.
109
109
110 :param engine:
110 :param engine:
111 The :class:`~sqlalchemy.engine.base.Engine` to use for table
111 The :class:`~sqlalchemy.engine.base.Engine` to use for table
112 reflection and schema alterations.
112 reflection and schema alterations.
113
113
114 :returns: A :class:`ColumnDelta` instance representing the change.
114 :returns: A :class:`ColumnDelta` instance representing the change.
115
115
116
116
117 """
117 """
118
118
119 if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
119 if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
120 k['table'] = p[0].table
120 k['table'] = p[0].table
121 if 'engine' not in k:
121 if 'engine' not in k:
122 k['engine'] = k['table'].bind
122 k['engine'] = k['table'].bind
123
123
124 # deprecation
124 # deprecation
125 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
125 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
126 warnings.warn(
126 warnings.warn(
127 "Passing a Column object to alter_column is deprecated."
127 "Passing a Column object to alter_column is deprecated."
128 " Just pass in keyword parameters instead.",
128 " Just pass in keyword parameters instead.",
129 MigrateDeprecationWarning
129 MigrateDeprecationWarning
130 )
130 )
131 engine = k['engine']
131 engine = k['engine']
132
132
133 # enough tests seem to break when metadata is always altered
133 # enough tests seem to break when metadata is always altered
134 # that this crutch has to be left in until they can be sorted
134 # that this crutch has to be left in until they can be sorted
135 # out
135 # out
136 k['alter_metadata']=True
136 k['alter_metadata']=True
137
137
138 delta = ColumnDelta(*p, **k)
138 delta = ColumnDelta(*p, **k)
139
139
140 visitorcallable = get_engine_visitor(engine, 'schemachanger')
140 visitorcallable = get_engine_visitor(engine, 'schemachanger')
141 _run_visitor(engine, visitorcallable, delta)
141 _run_visitor(engine, visitorcallable, delta)
142
142
143 return delta
143 return delta
144
144
145
145
146 def _to_table(table, engine=None):
146 def _to_table(table, engine=None):
147 """Return if instance of Table, else construct new with metadata"""
147 """Return if instance of Table, else construct new with metadata"""
148 if isinstance(table, sqlalchemy.Table):
148 if isinstance(table, sqlalchemy.Table):
149 return table
149 return table
150
150
151 # Given: table name, maybe an engine
151 # Given: table name, maybe an engine
152 meta = sqlalchemy.MetaData()
152 meta = sqlalchemy.MetaData()
153 if engine is not None:
153 if engine is not None:
154 meta.bind = engine
154 meta.bind = engine
155 return sqlalchemy.Table(table, meta)
155 return sqlalchemy.Table(table, meta)
156
156
157
157
158 def _to_index(index, table=None, engine=None):
158 def _to_index(index, table=None, engine=None):
159 """Return if instance of Index, else construct new with metadata"""
159 """Return if instance of Index, else construct new with metadata"""
160 if isinstance(index, sqlalchemy.Index):
160 if isinstance(index, sqlalchemy.Index):
161 return index
161 return index
162
162
163 # Given: index name; table name required
163 # Given: index name; table name required
164 table = _to_table(table, engine)
164 table = _to_table(table, engine)
165 ret = sqlalchemy.Index(index)
165 ret = sqlalchemy.Index(index)
166 ret.table = table
166 ret.table = table
167 return ret
167 return ret
168
168
169
169
170 def _run_visitor(
170 def _run_visitor(
171 connectable, visitorcallable, element, connection=None, **kwargs
171 connectable, visitorcallable, element, connection=None, **kwargs
172 ):
172 ):
173 if connection is not None:
173 if connection is not None:
174 visitorcallable(
174 visitorcallable(
175 connection.dialect, connection, **kwargs).traverse_single(element)
175 connection.dialect, connection, **kwargs).traverse_single(element)
176 else:
176 else:
177 conn = connectable.connect()
177 conn = connectable.connect()
178 try:
178 try:
179 visitorcallable(
179 visitorcallable(
180 conn.dialect, conn, **kwargs).traverse_single(element)
180 conn.dialect, conn, **kwargs).traverse_single(element)
181 finally:
181 finally:
182 conn.close()
182 conn.close()
183
183
184
184
185 # Python3: if we just use:
185 # Python3: if we just use:
186 #
186 #
187 # class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
187 # class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
188 # ...
188 # ...
189 #
189 #
190 # We get the following error:
190 # We get the following error:
191 # TypeError: metaclass conflict: the metaclass of a derived class must be a
191 # TypeError: metaclass conflict: the metaclass of a derived class must be a
192 # (non-strict) subclass of the metaclasses of all its bases.
192 # (non-strict) subclass of the metaclasses of all its bases.
193 #
193 #
194 # The complete inheritance/metaclass relationship list of ColumnDelta can be
194 # The complete inheritance/metaclass relationship list of ColumnDelta can be
195 # summarized by this following dot file:
195 # summarized by this following dot file:
196 #
196 #
197 # digraph test123 {
197 # digraph test123 {
198 # ColumnDelta -> MutableMapping;
198 # ColumnDelta -> MutableMapping;
199 # MutableMapping -> Mapping;
199 # MutableMapping -> Mapping;
200 # Mapping -> {Sized Iterable Container};
200 # Mapping -> {Sized Iterable Container};
201 # {Sized Iterable Container} -> ABCMeta[style=dashed];
201 # {Sized Iterable Container} -> ABCMeta[style=dashed];
202 #
202 #
203 # ColumnDelta -> SchemaItem;
203 # ColumnDelta -> SchemaItem;
204 # SchemaItem -> {SchemaEventTarget Visitable};
204 # SchemaItem -> {SchemaEventTarget Visitable};
205 # SchemaEventTarget -> object;
205 # SchemaEventTarget -> object;
206 # Visitable -> {VisitableType object} [style=dashed];
206 # Visitable -> {VisitableType object} [style=dashed];
207 # VisitableType -> type;
207 # VisitableType -> type;
208 # }
208 # }
209 #
209 #
210 # We need to use a metaclass that inherits from all the metaclasses of
210 # We need to use a metaclass that inherits from all the metaclasses of
211 # DictMixin and sqlalchemy.schema.SchemaItem. Let's call it "MyMeta".
211 # DictMixin and sqlalchemy.schema.SchemaItem. Let's call it "MyMeta".
212 class MyMeta(sqlalchemy.sql.visitors.VisitableType, abc.ABCMeta, object):
212 class MyMeta(sqlalchemy.sql.visitors.VisitableType, abc.ABCMeta, object):
213 pass
213 pass
214
214
215
215
216 class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem, metaclass=MyMeta):
216 class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem, metaclass=MyMeta):
217 """Extracts the differences between two columns/column-parameters
217 """Extracts the differences between two columns/column-parameters
218
218
219 May receive parameters arranged in several different ways:
219 May receive parameters arranged in several different ways:
220
220
221 * **current_column, new_column, \*p, \*\*kw**
221 * **current_column, new_column, \*p, \*\*kw**
222 Additional parameters can be specified to override column
222 Additional parameters can be specified to override column
223 differences.
223 differences.
224
224
225 * **current_column, \*p, \*\*kw**
225 * **current_column, \*p, \*\*kw**
226 Additional parameters alter current_column. Table name is extracted
226 Additional parameters alter current_column. Table name is extracted
227 from current_column object.
227 from current_column object.
228 Name is changed to current_column.name from current_name,
228 Name is changed to current_column.name from current_name,
229 if current_name is specified.
229 if current_name is specified.
230
230
231 * **current_col_name, \*p, \*\*kw**
231 * **current_col_name, \*p, \*\*kw**
232 Table kw must specified.
232 Table kw must specified.
233
233
234 :param table: Table at which current Column should be bound to.\
234 :param table: Table at which current Column should be bound to.\
235 If table name is given, reflection will be used.
235 If table name is given, reflection will be used.
236 :type table: string or Table instance
236 :type table: string or Table instance
237
237
238 :param metadata: A :class:`MetaData` instance to store
238 :param metadata: A :class:`MetaData` instance to store
239 reflected table names
239 reflected table names
240
240
241 :param engine: When reflecting tables, either engine or metadata must \
241 :param engine: When reflecting tables, either engine or metadata must \
242 be specified to acquire engine object.
242 be specified to acquire engine object.
243 :type engine: :class:`Engine` instance
243 :type engine: :class:`Engine` instance
244 :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
244 :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
245 `result_column` through :func:`dict` alike object.
245 `result_column` through :func:`dict` alike object.
246
246
247 * :class:`ColumnDelta`.result_column is altered column with new attributes
247 * :class:`ColumnDelta`.result_column is altered column with new attributes
248
248
249 * :class:`ColumnDelta`.current_name is current name of column in db
249 * :class:`ColumnDelta`.current_name is current name of column in db
250
250
251
251
252 """
252 """
253
253
254 # Column attributes that can be altered
254 # Column attributes that can be altered
255 diff_keys = ('name', 'type', 'primary_key', 'nullable',
255 diff_keys = ('name', 'type', 'primary_key', 'nullable',
256 'server_onupdate', 'server_default', 'autoincrement')
256 'server_onupdate', 'server_default', 'autoincrement')
257 diffs = dict()
257 diffs = dict()
258 __visit_name__ = 'column'
258 __visit_name__ = 'column'
259
259
260 def __init__(self, *p, **kw):
260 def __init__(self, *p, **kw):
261 # 'alter_metadata' is not a public api. It exists purely
261 # 'alter_metadata' is not a public api. It exists purely
262 # as a crutch until the tests that fail when 'alter_metadata'
262 # as a crutch until the tests that fail when 'alter_metadata'
263 # behaviour always happens can be sorted out
263 # behaviour always happens can be sorted out
264 self.alter_metadata = kw.pop("alter_metadata", False)
264 self.alter_metadata = kw.pop("alter_metadata", False)
265
265
266 self.meta = kw.pop("metadata", None)
266 self.meta = kw.pop("metadata", None)
267 self.engine = kw.pop("engine", None)
267 self.engine = kw.pop("engine", None)
268
268
269 # Things are initialized differently depending on how many column
269 # Things are initialized differently depending on how many column
270 # parameters are given. Figure out how many and call the appropriate
270 # parameters are given. Figure out how many and call the appropriate
271 # method.
271 # method.
272 if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
272 if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
273 # At least one column specified
273 # At least one column specified
274 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
274 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
275 # Two columns specified
275 # Two columns specified
276 diffs = self.compare_2_columns(*p, **kw)
276 diffs = self.compare_2_columns(*p, **kw)
277 else:
277 else:
278 # Exactly one column specified
278 # Exactly one column specified
279 diffs = self.compare_1_column(*p, **kw)
279 diffs = self.compare_1_column(*p, **kw)
280 else:
280 else:
281 # Zero columns specified
281 # Zero columns specified
282 if not len(p) or not isinstance(p[0], str):
282 if not len(p) or not isinstance(p[0], str):
283 raise ValueError("First argument must be column name")
283 raise ValueError("First argument must be column name")
284 diffs = self.compare_parameters(*p, **kw)
284 diffs = self.compare_parameters(*p, **kw)
285
285
286 self.apply_diffs(diffs)
286 self.apply_diffs(diffs)
287
287
288 def __repr__(self):
288 def __repr__(self):
289 return '<ColumnDelta altermetadata=%r, %s>' % (
289 return '<ColumnDelta altermetadata=%r, %s>' % (
290 self.alter_metadata,
290 self.alter_metadata,
291 super(ColumnDelta, self).__repr__()
291 super(ColumnDelta, self).__repr__()
292 )
292 )
293
293
294 def __getitem__(self, key):
294 def __getitem__(self, key):
295 if key not in list(self.keys()):
295 if key not in list(self.keys()):
296 raise KeyError("No such diff key, available: %s" % self.diffs )
296 raise KeyError("No such diff key, available: %s" % self.diffs )
297 return getattr(self.result_column, key)
297 return getattr(self.result_column, key)
298
298
299 def __setitem__(self, key, value):
299 def __setitem__(self, key, value):
300 if key not in list(self.keys()):
300 if key not in list(self.keys()):
301 raise KeyError("No such diff key, available: %s" % self.diffs )
301 raise KeyError("No such diff key, available: %s" % self.diffs )
302 setattr(self.result_column, key, value)
302 setattr(self.result_column, key, value)
303
303
304 def __delitem__(self, key):
304 def __delitem__(self, key):
305 raise NotImplementedError
305 raise NotImplementedError
306
306
307 def __len__(self):
307 def __len__(self):
308 raise NotImplementedError
308 raise NotImplementedError
309
309
310 def __iter__(self):
310 def __iter__(self):
311 raise NotImplementedError
311 raise NotImplementedError
312
312
313 def keys(self):
313 def keys(self):
314 return list(self.diffs.keys())
314 return list(self.diffs.keys())
315
315
316 def compare_parameters(self, current_name, *p, **k):
316 def compare_parameters(self, current_name, *p, **k):
317 """Compares Column objects with reflection"""
317 """Compares Column objects with reflection"""
318 self.table = k.pop('table')
318 self.table = k.pop('table')
319 self.result_column = self._table.c.get(current_name)
319 self.result_column = self._table.c.get(current_name)
320 if len(p):
320 if len(p):
321 k = self._extract_parameters(p, k, self.result_column)
321 k = self._extract_parameters(p, k, self.result_column)
322 return k
322 return k
323
323
324 def compare_1_column(self, col, *p, **k):
324 def compare_1_column(self, col, *p, **k):
325 """Compares one Column object"""
325 """Compares one Column object"""
326 self.table = k.pop('table', None)
326 self.table = k.pop('table', None)
327 if self.table is None:
327 if self.table is None:
328 self.table = col.table
328 self.table = col.table
329 self.result_column = col
329 self.result_column = col
330 if len(p):
330 if len(p):
331 k = self._extract_parameters(p, k, self.result_column)
331 k = self._extract_parameters(p, k, self.result_column)
332 return k
332 return k
333
333
334 def compare_2_columns(self, old_col, new_col, *p, **k):
334 def compare_2_columns(self, old_col, new_col, *p, **k):
335 """Compares two Column objects"""
335 """Compares two Column objects"""
336 self.process_column(new_col)
336 self.process_column(new_col)
337 self.table = k.pop('table', None)
337 self.table = k.pop('table', None)
338 # we cannot use bool() on table in SA06
338 # we cannot use bool() on table in SA06
339 if self.table is None:
339 if self.table is None:
340 self.table = old_col.table
340 self.table = old_col.table
341 if self.table is None:
341 if self.table is None:
342 new_col.table
342 new_col.table
343 self.result_column = old_col
343 self.result_column = old_col
344
344
345 # set differences
345 # set differences
346 # leave out some stuff for later comp
346 # leave out some stuff for later comp
347 for key in (set(self.diff_keys) - set(('type',))):
347 for key in (set(self.diff_keys) - set(('type',))):
348 val = getattr(new_col, key, None)
348 val = getattr(new_col, key, None)
349 if getattr(self.result_column, key, None) != val:
349 if getattr(self.result_column, key, None) != val:
350 k.setdefault(key, val)
350 k.setdefault(key, val)
351
351
352 # inspect types
352 # inspect types
353 if not self.are_column_types_eq(self.result_column.type, new_col.type):
353 if not self.are_column_types_eq(self.result_column.type, new_col.type):
354 k.setdefault('type', new_col.type)
354 k.setdefault('type', new_col.type)
355
355
356 if len(p):
356 if len(p):
357 k = self._extract_parameters(p, k, self.result_column)
357 k = self._extract_parameters(p, k, self.result_column)
358 return k
358 return k
359
359
360 def apply_diffs(self, diffs):
360 def apply_diffs(self, diffs):
361 """Populate dict and column object with new values"""
361 """Populate dict and column object with new values"""
362 self.diffs = diffs
362 self.diffs = diffs
363 for key in self.diff_keys:
363 for key in self.diff_keys:
364 if key in diffs:
364 if key in diffs:
365 setattr(self.result_column, key, diffs[key])
365 setattr(self.result_column, key, diffs[key])
366
366
367 self.process_column(self.result_column)
367 self.process_column(self.result_column)
368
368
369 # create an instance of class type if not yet
369 # create an instance of class type if not yet
370 if 'type' in diffs and callable(self.result_column.type):
370 if 'type' in diffs:
371 if callable(self.result_column.type):
371 self.result_column.type = self.result_column.type()
372 self.result_column.type = self.result_column.type()
373 if self.result_column.autoincrement and \
374 not issubclass(
375 self.result_column.type._type_affinity,
376 sqlalchemy.Integer):
377 self.result_column.autoincrement = False
372
378
373 # add column to the table
379 # add column to the table
374 if self.table is not None and self.alter_metadata:
380 if self.table is not None and self.alter_metadata:
375 self.result_column.add_to_table(self.table)
381 self.result_column.add_to_table(self.table)
376
382
377 def are_column_types_eq(self, old_type, new_type):
383 def are_column_types_eq(self, old_type, new_type):
378 """Compares two types to be equal"""
384 """Compares two types to be equal"""
379 ret = old_type.__class__ == new_type.__class__
385 ret = old_type.__class__ == new_type.__class__
380
386
381 # String length is a special case
387 # String length is a special case
382 if ret and isinstance(new_type, sqlalchemy.types.String):
388 if ret and isinstance(new_type, sqlalchemy.types.String):
383 ret = (getattr(old_type, 'length', None) == \
389 ret = (getattr(old_type, 'length', None) == \
384 getattr(new_type, 'length', None))
390 getattr(new_type, 'length', None))
385 return ret
391 return ret
386
392
387 def _extract_parameters(self, p, k, column):
393 def _extract_parameters(self, p, k, column):
388 """Extracts data from p and modifies diffs"""
394 """Extracts data from p and modifies diffs"""
389 p = list(p)
395 p = list(p)
390 while len(p):
396 while len(p):
391 if isinstance(p[0], str):
397 if isinstance(p[0], str):
392 k.setdefault('name', p.pop(0))
398 k.setdefault('name', p.pop(0))
393 elif isinstance(p[0], sqlalchemy.types.TypeEngine):
399 elif isinstance(p[0], sqlalchemy.types.TypeEngine):
394 k.setdefault('type', p.pop(0))
400 k.setdefault('type', p.pop(0))
395 elif callable(p[0]):
401 elif callable(p[0]):
396 p[0] = p[0]()
402 p[0] = p[0]()
397 else:
403 else:
398 break
404 break
399
405
400 if len(p):
406 if len(p):
401 new_col = column.copy_fixed()
407 new_col = column.copy_fixed()
402 new_col._init_items(*p)
408 new_col._init_items(*p)
403 k = self.compare_2_columns(column, new_col, **k)
409 k = self.compare_2_columns(column, new_col, **k)
404 return k
410 return k
405
411
406 def process_column(self, column):
412 def process_column(self, column):
407 """Processes default values for column"""
413 """Processes default values for column"""
408 # XXX: this is a snippet from SA processing of positional parameters
414 # XXX: this is a snippet from SA processing of positional parameters
409 toinit = list()
415 toinit = list()
410
416
411 if column.server_default is not None:
417 if column.server_default is not None:
412 if isinstance(column.server_default, sqlalchemy.FetchedValue):
418 if isinstance(column.server_default, sqlalchemy.FetchedValue):
413 toinit.append(column.server_default)
419 toinit.append(column.server_default)
414 else:
420 else:
415 toinit.append(sqlalchemy.DefaultClause(column.server_default))
421 toinit.append(sqlalchemy.DefaultClause(column.server_default))
416 if column.server_onupdate is not None:
422 if column.server_onupdate is not None:
417 if isinstance(column.server_onupdate, FetchedValue):
423 if isinstance(column.server_onupdate, FetchedValue):
418 toinit.append(column.server_default)
424 toinit.append(column.server_default)
419 else:
425 else:
420 toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
426 toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
421 for_update=True))
427 for_update=True))
422 if toinit:
428 if toinit:
423 column._init_items(*toinit)
429 column._init_items(*toinit)
424
430
425 def _get_table(self):
431 def _get_table(self):
426 return getattr(self, '_table', None)
432 return getattr(self, '_table', None)
427
433
428 def _set_table(self, table):
434 def _set_table(self, table):
429 if isinstance(table, str):
435 if isinstance(table, str):
430 if self.alter_metadata:
436 if self.alter_metadata:
431 if not self.meta:
437 if not self.meta:
432 raise ValueError("metadata must be specified for table"
438 raise ValueError("metadata must be specified for table"
433 " reflection when using alter_metadata")
439 " reflection when using alter_metadata")
434 meta = self.meta
440 meta = self.meta
435 if self.engine:
441 if self.engine:
436 meta.bind = self.engine
442 meta.bind = self.engine
437 else:
443 else:
438 if not self.engine and not self.meta:
444 if not self.engine and not self.meta:
439 raise ValueError("engine or metadata must be specified"
445 raise ValueError("engine or metadata must be specified"
440 " to reflect tables")
446 " to reflect tables")
441 if not self.engine:
447 if not self.engine:
442 self.engine = self.meta.bind
448 self.engine = self.meta.bind
443 meta = sqlalchemy.MetaData(bind=self.engine)
449 meta = sqlalchemy.MetaData(bind=self.engine)
444 self._table = sqlalchemy.Table(table, meta, autoload=True)
450 self._table = sqlalchemy.Table(table, meta, autoload=True)
445 elif isinstance(table, sqlalchemy.Table):
451 elif isinstance(table, sqlalchemy.Table):
446 self._table = table
452 self._table = table
447 if not self.alter_metadata:
453 if not self.alter_metadata:
448 self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
454 self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
449 def _get_result_column(self):
455 def _get_result_column(self):
450 return getattr(self, '_result_column', None)
456 return getattr(self, '_result_column', None)
451
457
452 def _set_result_column(self, column):
458 def _set_result_column(self, column):
453 """Set Column to Table based on alter_metadata evaluation."""
459 """Set Column to Table based on alter_metadata evaluation."""
454 self.process_column(column)
460 self.process_column(column)
455 if not hasattr(self, 'current_name'):
461 if not hasattr(self, 'current_name'):
456 self.current_name = column.name
462 self.current_name = column.name
457 if self.alter_metadata:
463 if self.alter_metadata:
458 self._result_column = column
464 self._result_column = column
459 else:
465 else:
460 self._result_column = column.copy_fixed()
466 self._result_column = column.copy_fixed()
461
467
462 table = property(_get_table, _set_table)
468 table = property(_get_table, _set_table)
463 result_column = property(_get_result_column, _set_result_column)
469 result_column = property(_get_result_column, _set_result_column)
464
470
465
471
466 class ChangesetTable(object):
472 class ChangesetTable(object):
467 """Changeset extensions to SQLAlchemy tables."""
473 """Changeset extensions to SQLAlchemy tables."""
468
474
469 def create_column(self, column, *p, **kw):
475 def create_column(self, column, *p, **kw):
470 """Creates a column.
476 """Creates a column.
471
477
472 The column parameter may be a column definition or the name of
478 The column parameter may be a column definition or the name of
473 a column in this table.
479 a column in this table.
474
480
475 API to :meth:`ChangesetColumn.create`
481 API to :meth:`ChangesetColumn.create`
476
482
477 :param column: Column to be created
483 :param column: Column to be created
478 :type column: Column instance or string
484 :type column: Column instance or string
479 """
485 """
480 if not isinstance(column, sqlalchemy.Column):
486 if not isinstance(column, sqlalchemy.Column):
481 # It's a column name
487 # It's a column name
482 column = getattr(self.c, str(column))
488 column = getattr(self.c, str(column))
483 column.create(table=self, *p, **kw)
489 column.create(table=self, *p, **kw)
484
490
485 def drop_column(self, column, *p, **kw):
491 def drop_column(self, column, *p, **kw):
486 """Drop a column, given its name or definition.
492 """Drop a column, given its name or definition.
487
493
488 API to :meth:`ChangesetColumn.drop`
494 API to :meth:`ChangesetColumn.drop`
489
495
490 :param column: Column to be droped
496 :param column: Column to be droped
491 :type column: Column instance or string
497 :type column: Column instance or string
492 """
498 """
493 if not isinstance(column, sqlalchemy.Column):
499 if not isinstance(column, sqlalchemy.Column):
494 # It's a column name
500 # It's a column name
495 try:
501 try:
496 column = getattr(self.c, str(column))
502 column = getattr(self.c, str(column))
497 except AttributeError:
503 except AttributeError:
498 # That column isn't part of the table. We don't need
504 # That column isn't part of the table. We don't need
499 # its entire definition to drop the column, just its
505 # its entire definition to drop the column, just its
500 # name, so create a dummy column with the same name.
506 # name, so create a dummy column with the same name.
501 column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
507 column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
502 column.drop(table=self, *p, **kw)
508 column.drop(table=self, *p, **kw)
503
509
504 def rename(self, name, connection=None, **kwargs):
510 def rename(self, name, connection=None, **kwargs):
505 """Rename this table.
511 """Rename this table.
506
512
507 :param name: New name of the table.
513 :param name: New name of the table.
508 :type name: string
514 :type name: string
509 :param connection: reuse connection istead of creating new one.
515 :param connection: reuse connection istead of creating new one.
510 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
516 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
511 """
517 """
512 engine = self.bind
518 engine = self.bind
513 self.new_name = name
519 self.new_name = name
514 visitorcallable = get_engine_visitor(engine, 'schemachanger')
520 visitorcallable = get_engine_visitor(engine, 'schemachanger')
515 run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
521 run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
516
522
517 # Fix metadata registration
523 # Fix metadata registration
518 self.name = name
524 self.name = name
519 self.deregister()
525 self.deregister()
520 self._set_parent(self.metadata)
526 self._set_parent(self.metadata)
521
527
522 def _meta_key(self):
528 def _meta_key(self):
523 """Get the meta key for this table."""
529 """Get the meta key for this table."""
524 return sqlalchemy.schema._get_table_key(self.name, self.schema)
530 return sqlalchemy.schema._get_table_key(self.name, self.schema)
525
531
526 def deregister(self):
532 def deregister(self):
527 """Remove this table from its metadata"""
533 """Remove this table from its metadata"""
528 if SQLA_07:
534 if SQLA_07:
529 self.metadata._remove_table(self.name, self.schema)
535 self.metadata._remove_table(self.name, self.schema)
530 else:
536 else:
531 key = self._meta_key()
537 key = self._meta_key()
532 meta = self.metadata
538 meta = self.metadata
533 if key in meta.tables:
539 if key in meta.tables:
534 del meta.tables[key]
540 del meta.tables[key]
535
541
536
542
537 class ChangesetColumn(object):
543 class ChangesetColumn(object):
538 """Changeset extensions to SQLAlchemy columns."""
544 """Changeset extensions to SQLAlchemy columns."""
539
545
540 def alter(self, *p, **k):
546 def alter(self, *p, **k):
541 """Makes a call to :func:`alter_column` for the column this
547 """Makes a call to :func:`alter_column` for the column this
542 method is called on.
548 method is called on.
543 """
549 """
544 if 'table' not in k:
550 if 'table' not in k:
545 k['table'] = self.table
551 k['table'] = self.table
546 if 'engine' not in k:
552 if 'engine' not in k:
547 k['engine'] = k['table'].bind
553 k['engine'] = k['table'].bind
548 return alter_column(self, *p, **k)
554 return alter_column(self, *p, **k)
549
555
550 def create(self, table=None, index_name=None, unique_name=None,
556 def create(self, table=None, index_name=None, unique_name=None,
551 primary_key_name=None, populate_default=True, connection=None, **kwargs):
557 primary_key_name=None, populate_default=True, connection=None, **kwargs):
552 """Create this column in the database.
558 """Create this column in the database.
553
559
554 Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
560 Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
555 for most databases.
561 for most databases.
556
562
557 :param table: Table instance to create on.
563 :param table: Table instance to create on.
558 :param index_name: Creates :class:`ChangesetIndex` on this column.
564 :param index_name: Creates :class:`ChangesetIndex` on this column.
559 :param unique_name: Creates :class:\
565 :param unique_name: Creates :class:\
560 `~migrate.changeset.constraint.UniqueConstraint` on this column.
566 `~migrate.changeset.constraint.UniqueConstraint` on this column.
561 :param primary_key_name: Creates :class:\
567 :param primary_key_name: Creates :class:\
562 `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column.
568 `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column.
563 :param populate_default: If True, created column will be \
569 :param populate_default: If True, created column will be \
564 populated with defaults
570 populated with defaults
565 :param connection: reuse connection istead of creating new one.
571 :param connection: reuse connection istead of creating new one.
566 :type table: Table instance
572 :type table: Table instance
567 :type index_name: string
573 :type index_name: string
568 :type unique_name: string
574 :type unique_name: string
569 :type primary_key_name: string
575 :type primary_key_name: string
570 :type populate_default: bool
576 :type populate_default: bool
571 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
577 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
572
578
573 :returns: self
579 :returns: self
574 """
580 """
575 self.populate_default = populate_default
581 self.populate_default = populate_default
576 self.index_name = index_name
582 self.index_name = index_name
577 self.unique_name = unique_name
583 self.unique_name = unique_name
578 self.primary_key_name = primary_key_name
584 self.primary_key_name = primary_key_name
579 for cons in ('index_name', 'unique_name', 'primary_key_name'):
585 for cons in ('index_name', 'unique_name', 'primary_key_name'):
580 self._check_sanity_constraints(cons)
586 self._check_sanity_constraints(cons)
581
587
582 self.add_to_table(table)
588 self.add_to_table(table)
583 engine = self.table.bind
589 engine = self.table.bind
584 visitorcallable = get_engine_visitor(engine, 'columngenerator')
590 visitorcallable = get_engine_visitor(engine, 'columngenerator')
585 _run_visitor(engine, visitorcallable, self, connection, **kwargs)
591 _run_visitor(engine, visitorcallable, self, connection, **kwargs)
586
592
587 # TODO: reuse existing connection
593 # TODO: reuse existing connection
588 if self.populate_default and self.default is not None:
594 if self.populate_default and self.default is not None:
589 stmt = table.update().values({self: engine._execute_default(self.default)})
595 stmt = table.update().values({self: engine._execute_default(self.default)})
590 engine.execute(stmt)
596 engine.execute(stmt)
591
597
592 return self
598 return self
593
599
594 def drop(self, table=None, connection=None, **kwargs):
600 def drop(self, table=None, connection=None, **kwargs):
595 """Drop this column from the database, leaving its table intact.
601 """Drop this column from the database, leaving its table intact.
596
602
597 ``ALTER TABLE DROP COLUMN``, for most databases.
603 ``ALTER TABLE DROP COLUMN``, for most databases.
598
604
599 :param connection: reuse connection istead of creating new one.
605 :param connection: reuse connection istead of creating new one.
600 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
606 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
601 """
607 """
602 if table is not None:
608 if table is not None:
603 self.table = table
609 self.table = table
604 engine = self.table.bind
610 engine = self.table.bind
605 visitorcallable = get_engine_visitor(engine, 'columndropper')
611 visitorcallable = get_engine_visitor(engine, 'columndropper')
606 _run_visitor(engine, visitorcallable, self, connection, **kwargs)
612 _run_visitor(engine, visitorcallable, self, connection, **kwargs)
607 self.remove_from_table(self.table, unset_table=False)
613 self.remove_from_table(self.table, unset_table=False)
608 self.table = None
614 self.table = None
609 return self
615 return self
610
616
611 def add_to_table(self, table):
617 def add_to_table(self, table):
612 if table is not None and self.table is None:
618 if table is not None and self.table is None:
613 if SQLA_07:
619 if SQLA_07:
614 table.append_column(self)
620 table.append_column(self)
615 else:
621 else:
616 self._set_parent(table)
622 self._set_parent(table)
617
623
618 def _col_name_in_constraint(self,cons,name):
624 def _col_name_in_constraint(self,cons,name):
619 return False
625 return False
620
626
621 def remove_from_table(self, table, unset_table=True):
627 def remove_from_table(self, table, unset_table=True):
622 # TODO: remove primary keys, constraints, etc
628 # TODO: remove primary keys, constraints, etc
623 if unset_table:
629 if unset_table:
624 self.table = None
630 self.table = None
625
631
626 to_drop = set()
632 to_drop = set()
627 for index in table.indexes:
633 for index in table.indexes:
628 columns = []
634 columns = []
629 for col in index.columns:
635 for col in index.columns:
630 if col.name!=self.name:
636 if col.name!=self.name:
631 columns.append(col)
637 columns.append(col)
632 if columns:
638 if columns:
633 index.columns = columns
639 index.columns = columns
634 if SQLA_08:
640 if SQLA_08:
635 index.expressions = columns
641 index.expressions = columns
636 else:
642 else:
637 to_drop.add(index)
643 to_drop.add(index)
638 table.indexes = table.indexes - to_drop
644 table.indexes = table.indexes - to_drop
639
645
640 to_drop = set()
646 to_drop = set()
641 for cons in table.constraints:
647 for cons in table.constraints:
642 # TODO: deal with other types of constraint
648 # TODO: deal with other types of constraint
643 if isinstance(cons,(ForeignKeyConstraint,
649 if isinstance(cons,(ForeignKeyConstraint,
644 UniqueConstraint)):
650 UniqueConstraint)):
645 for col_name in cons.columns:
651 for col_name in cons.columns:
646 if not isinstance(col_name, str):
652 if not isinstance(col_name, str):
647 col_name = col_name.name
653 col_name = col_name.name
648 if self.name==col_name:
654 if self.name==col_name:
649 to_drop.add(cons)
655 to_drop.add(cons)
650 table.constraints = table.constraints - to_drop
656 table.constraints = table.constraints - to_drop
651
657
652 if table.c.contains_column(self):
658 if table.c.contains_column(self):
653 if SQLA_07:
659 if SQLA_07:
654 table._columns.remove(self)
660 table._columns.remove(self)
655 else:
661 else:
656 table.c.remove(self)
662 table.c.remove(self)
657
663
658 # TODO: this is fixed in 0.6
664 # TODO: this is fixed in 0.6
659 def copy_fixed(self, **kw):
665 def copy_fixed(self, **kw):
660 """Create a copy of this ``Column``, with all attributes."""
666 """Create a copy of this ``Column``, with all attributes."""
661 q = util.safe_quote(self)
667 q = util.safe_quote(self)
662 return sqlalchemy.Column(self.name, self.type, self.default,
668 return sqlalchemy.Column(self.name, self.type, self.default,
663 key=self.key,
669 key=self.key,
664 primary_key=self.primary_key,
670 primary_key=self.primary_key,
665 nullable=self.nullable,
671 nullable=self.nullable,
666 quote=q,
672 quote=q,
667 index=self.index,
673 index=self.index,
668 unique=self.unique,
674 unique=self.unique,
669 onupdate=self.onupdate,
675 onupdate=self.onupdate,
670 autoincrement=self.autoincrement,
676 autoincrement=self.autoincrement,
671 server_default=self.server_default,
677 server_default=self.server_default,
672 server_onupdate=self.server_onupdate,
678 server_onupdate=self.server_onupdate,
673 *[c.copy(**kw) for c in self.constraints])
679 *[c.copy(**kw) for c in self.constraints])
674
680
675 def _check_sanity_constraints(self, name):
681 def _check_sanity_constraints(self, name):
676 """Check if constraints names are correct"""
682 """Check if constraints names are correct"""
677 obj = getattr(self, name)
683 obj = getattr(self, name)
678 if (getattr(self, name[:-5]) and not obj):
684 if (getattr(self, name[:-5]) and not obj):
679 raise InvalidConstraintError("Column.create() accepts index_name,"
685 raise InvalidConstraintError("Column.create() accepts index_name,"
680 " primary_key_name and unique_name to generate constraints")
686 " primary_key_name and unique_name to generate constraints")
681 if not isinstance(obj, str) and obj is not None:
687 if not isinstance(obj, str) and obj is not None:
682 raise InvalidConstraintError(
688 raise InvalidConstraintError(
683 "%s argument for column must be constraint name" % name)
689 "%s argument for column must be constraint name" % name)
684
690
685
691
686 class ChangesetIndex(object):
692 class ChangesetIndex(object):
687 """Changeset extensions to SQLAlchemy Indexes."""
693 """Changeset extensions to SQLAlchemy Indexes."""
688
694
689 __visit_name__ = 'index'
695 __visit_name__ = 'index'
690
696
691 def rename(self, name, connection=None, **kwargs):
697 def rename(self, name, connection=None, **kwargs):
692 """Change the name of an index.
698 """Change the name of an index.
693
699
694 :param name: New name of the Index.
700 :param name: New name of the Index.
695 :type name: string
701 :type name: string
696 :param connection: reuse connection istead of creating new one.
702 :param connection: reuse connection istead of creating new one.
697 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
703 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
698 """
704 """
699 engine = self.table.bind
705 engine = self.table.bind
700 self.new_name = name
706 self.new_name = name
701 visitorcallable = get_engine_visitor(engine, 'schemachanger')
707 visitorcallable = get_engine_visitor(engine, 'schemachanger')
702 engine._run_visitor(visitorcallable, self, connection, **kwargs)
708 engine._run_visitor(visitorcallable, self, connection, **kwargs)
703 self.name = name
709 self.name = name
704
710
705
711
706 class ChangesetDefaultClause(object):
712 class ChangesetDefaultClause(object):
707 """Implements comparison between :class:`DefaultClause` instances"""
713 """Implements comparison between :class:`DefaultClause` instances"""
708
714
709 def __eq__(self, other):
715 def __eq__(self, other):
710 if isinstance(other, self.__class__):
716 if isinstance(other, self.__class__):
711 if self.arg == other.arg:
717 if self.arg == other.arg:
712 return True
718 return True
713
719
714 def __ne__(self, other):
720 def __ne__(self, other):
715 return not self.__eq__(other)
721 return not self.__eq__(other)
General Comments 0
You need to be logged in to leave comments. Login now