Commit 9f7ab968 authored by iElectric's avatar iElectric

- completely refactored ColumnDelta to extract differences between...

- completely refactored ColumnDelta to extract differences between columns/parameters (also fixes issue #23)
- fixed some bugs (passing server_default) on column.alter
- updated tests, specially ColumnDelta and column.alter
- introduced alter_metadata which can preserve altering existing objects if False (defaults to True)
- updated documentation
parent a8c31eb2
- better MySQL support
- fix unit tests for other databases than PostgreSQL (MySQL and SQLite
fail at test_changeset.test_fk(..))
- better SQL scripts support (testing, source viewing)
make_update_script_for_model:
......@@ -9,9 +5,4 @@ make_update_script_for_model:
- columns are not compared?
- even if two "models" are equal, it doesn't yield so
- refactor test_shell to test_api and use TestScript for cmd line testing
- controlledschema.drop() drops whole migrate table, maybe there are some other repositories bound to it!
- document sqlite hacks (unique index for pk constraint)
- document constraints usage, document all ways then can be used, document cascade,table,columns options
0.5.5
-----
- alter column constructs now accept `alter_metadata` parameter. If True, it will modify Column/Table objects according to changes. Otherwise, everything will be untouched.
- complete refactoring of :class:`~migrate.changeset.schema.ColumnDelta` (fixes issue 23)
- added support for :ref:`firebird <firebird-d>`
- fixed bug when column.alter(server_default='string') was not properly set
- server_defaults passed to column.create are now issued correctly
- constraints passed to column.create are correctly interpreted (ALTER TABLE ADD CONSTRAINT is issued after ADD COLUMN)
- column.create accepts `primary_key_name`, `unique_name` and `index_name` as string value which is used as contraint name when adding a column
......@@ -18,6 +21,7 @@
**Backward incompatible changes**:
- python upgrade/downgrade scripts do not import migrate_engine magically, but recieve engine as the only parameter to function
- alter column does not accept `current_name` anymore, it extracts name from the old column.
0.5.4
-----
......
......@@ -59,8 +59,8 @@ Dialect support
| :ref:`ALTER TABLE DROP COLUMN <column-drop>` | yes | yes | yes | yes | yes | |
| | (workaround) [#1]_ | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| :ref:`ALTER TABLE ALTER COLUMN <column-alter>` | no | yes | yes | yes | yes [#4]_ | |
| | | | | (with limitations) [#3]_ | | |
| :ref:`ALTER TABLE ALTER COLUMN <column-alter>` | yes | yes | yes | yes | yes [#4]_ | |
| | (workaround) [#1]_ | | | (with limitations) [#3]_ | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| :ref:`ALTER TABLE ADD CONSTRAINT <constraint-tutorial>` | no | yes | yes | yes | yes | |
| | | | | | | |
......
......@@ -12,3 +12,5 @@ from migrate.changeset.constraint import *
sqlalchemy.schema.Table.__bases__ += (ChangesetTable, )
sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, )
sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, )
sqlalchemy.schema.DefaultClause.__bases__ += (ChangesetDefaultClause, )
......@@ -45,30 +45,6 @@ class AlterTableVisitor(SchemaIterator):
self.append('\nALTER TABLE %s ' % self.preparer.format_table(table))
return table
# DEPRECATED: use plain constraints instead
#def _pk_constraint(self, table, column, status):
# """Create a primary key constraint from a table, column.
# Status: true if the constraint is being added; false if being dropped
# """
# if isinstance(column, basestring):
# column = getattr(table.c, name)
# ret = constraint.PrimaryKeyConstraint(*table.primary_key)
# if status:
# # Created PK
# ret.c.append(column)
# else:
# # Dropped PK
# names = [c.name for c in cons.c]
# index = names.index(col.name)
# del ret.c[index]
# # Allow explicit PK name assignment
# if isinstance(pk, basestring):
# ret.name = pk
# return ret
class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator):
"""Extends ansisql generator for column creation (alter table add col)"""
......@@ -160,10 +136,9 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
True), index.quote)))
self.execute()
def visit_column(self, column):
def visit_column(self, delta):
"""Rename/change a column."""
# ALTER COLUMN is implemented as several ALTER statements
delta = column.delta
keys = delta.keys()
if 'type' in keys:
self._run_subvisit(delta, self._visit_column_type)
......@@ -182,44 +157,37 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
col_name = delta.current_name
if start_alter:
self.start_alter_column(table, col_name)
ret = func(table, col_name, delta)
ret = func(table, delta.result_column, delta)
self.execute()
def start_alter_column(self, table, col_name):
"""Starts ALTER COLUMN"""
self.start_alter_table(table)
# TODO: use preparer.format_column
self.append("ALTER COLUMN %s " % self.preparer.quote(col_name, table.quote))
def _visit_column_nullable(self, table, col_name, delta):
def _visit_column_nullable(self, table, column, delta):
nullable = delta['nullable']
if nullable:
self.append("DROP NOT NULL")
else:
self.append("SET NOT NULL")
def _visit_column_default(self, table, col_name, delta):
server_default = delta['server_default']
# Dummy column: get_col_default_string needs a column for some
# reason
dummy = sa.Column(None, None, server_default=server_default)
default_text = self.get_column_default_string(dummy)
def _visit_column_default(self, table, column, delta):
default_text = self.get_column_default_string(column)
if default_text is not None:
self.append("SET DEFAULT %s" % default_text)
else:
self.append("DROP DEFAULT")
def _visit_column_type(self, table, col_name, delta):
def _visit_column_type(self, table, column, delta):
type_ = delta['type']
if not isinstance(type_, sa.types.AbstractType):
# It's the class itself, not an instance... make an instance
type_ = type_()
type_text = type_.dialect_impl(self.dialect).get_col_spec()
self.append("TYPE %s" % type_text)
def _visit_column_name(self, table, col_name, delta):
new_name = delta['name']
def _visit_column_name(self, table, column, delta):
self.start_alter_table(table)
col_name = self.preparer.quote(delta.current_name, table.quote)
new_name = self.preparer.format_column(delta.result_column)
self.append('RENAME COLUMN %s TO %s' % (col_name, new_name))
......
......@@ -30,12 +30,13 @@ class FBSchemaChanger(ansisql.ANSISchemaChanger):
raise exceptions.NotSupportedError(
"Firebird does not support renaming tables.")
def _visit_column_name(self, table, col_name, delta):
new_name = delta['name']
def _visit_column_name(self, table, column, delta):
self.start_alter_table(table)
self.append('ALTER COLUMN %s TO %s' % ((col_name), (new_name)))
col_name = self.preparer.quote(delta.current_name, table.quote)
new_name = self.preparer.format_column(delta.result_column)
self.append('ALTER COLUMN %s TO %s' % (col_name, new_name))
def _visit_column_nullable(self, table, col_name, delta):
def _visit_column_nullable(self, table, column, delta):
"""Changing NULL is not supported"""
# TODO: http://www.firebirdfaq.org/faq103/
raise exceptions.NotSupportedError(
......@@ -50,6 +51,7 @@ class FBConstraintDropper(ansisql.ANSIConstraintDropper):
"""Firebird constaint dropper implementation."""
def cascade_constraint(self, constraint):
"""Cascading constraints is not supported"""
raise exceptions.NotSupportedError(
"Firebird does not support cascading constraints")
......
......@@ -20,19 +20,13 @@ class MySQLColumnDropper(ansisql.ANSIColumnDropper):
class MySQLSchemaChanger(MySQLSchemaGenerator, ansisql.ANSISchemaChanger):
def visit_column(self, column):
delta = column.delta
table = column.table
colspec = self.get_column_specification(column)
if not hasattr(delta, 'result_column'):
# Mysql needs the whole column definition, not just a lone name/type
raise exceptions.NotSupportedError(
"A column object must be present in table to alter it")
def visit_column(self, delta):
table = delta.table
colspec = self.get_column_specification(delta.result_column)
old_col_name = self.preparer.quote(delta.current_name, table.quote)
self.start_alter_table(table)
old_col_name = self.preparer.quote(delta.current_name, column.quote)
self.append("CHANGE COLUMN %s " % old_col_name)
self.append(colspec)
self.execute()
......
......@@ -32,27 +32,20 @@ class OracleSchemaChanger(OracleSchemaGenerator, ansisql.ANSISchemaChanger):
column.nullable = orig
return ret
def visit_column(self, column):
delta = column.delta
def visit_column(self, delta):
keys = delta.keys()
if len(set(('type', 'nullable', 'server_default')).intersection(keys)):
self._run_subvisit(delta,
self._visit_column_change,
start_alter=False)
# change name as the last action to avoid conflicts
if 'name' in keys:
self._run_subvisit(delta,
self._visit_column_name,
start_alter=False)
def _visit_column_change(self, table, col_name, delta):
if not hasattr(delta, 'result_column'):
# Oracle needs the whole column definition, not just a lone name/type
raise exceptions.NotSupportedError(
"A column object must be present in table to alter it")
if len(set(('type', 'nullable', 'server_default')).intersection(keys)):
self._run_subvisit(delta,
self._visit_column_change,
start_alter=False)
column = delta.result_column
def _visit_column_change(self, table, column, delta):
# Oracle cannot drop a default once created, but it can set it
# to null. We'll do that if default=None
# http://forums.oracle.com/forums/message.jspa?messageID=1273234#1273234
......
......@@ -3,6 +3,9 @@
.. _`SQLite`: http://www.sqlite.org/
"""
from UserDict import DictMixin
from copy import copy
from sqlalchemy.databases import sqlite as sa_base
from migrate.changeset import ansisql, exceptions
......@@ -19,18 +22,25 @@ class SQLiteCommon(object):
class SQLiteHelper(SQLiteCommon):
def visit_column(self, column):
table = self._to_table(column.table)
def visit_column(self, delta):
if isinstance(delta, DictMixin):
column = delta.result_column
table = self._to_table(delta.table)
else:
column = delta
table = self._to_table(column.table)
table_name = self.preparer.format_table(table)
# we remove all constraints, indexes so it doesnt recreate them
ixbackup = copy(table.indexes)
consbackup = copy(table.constraints)
table.indexes = set()
table.constraints = set()
self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
self.execute()
insertion_string = self._modify_table(table, column)
insertion_string = self._modify_table(table, column, delta)
table.create()
self.append(insertion_string % {'table_name': table_name})
......@@ -38,6 +48,10 @@ class SQLiteHelper(SQLiteCommon):
self.append('DROP TABLE migration_tmp')
self.execute()
# restore indexes, constraints
table.indexes = ixbackup
table.constraints = consbackup
class SQLiteColumnGenerator(SQLiteSchemaGenerator, SQLiteCommon,
ansisql.ANSIColumnGenerator):
......@@ -51,7 +65,7 @@ class SQLiteColumnGenerator(SQLiteSchemaGenerator, SQLiteCommon,
class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
"""SQLite ColumnDropper"""
def _modify_table(self, table, column):
def _modify_table(self, table, column, delta):
columns = ' ,'.join(map(self.preparer.format_column, table.columns))
return 'INSERT INTO %(table_name)s SELECT ' + columns + \
' from migration_tmp'
......@@ -60,11 +74,8 @@ class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
"""SQLite SchemaChanger"""
def _modify_table(self, table, column):
delta = column.delta
def _modify_table(self, table, column, delta):
column = table.columns[delta.current_name]
for k, v in delta.items():
setattr(column, k, v)
return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
def visit_index(self, index):
......@@ -94,6 +105,7 @@ class SQLiteConstraintDropper(ansisql.ANSIColumnDropper, ansisql.ANSIConstraintC
self.execute()
# TODO: add not_supported tags for constraint dropper/generator
# TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index
class SQLiteDialect(ansisql.ANSIDialect):
columngenerator = SQLiteColumnGenerator
......
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment