diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-06-23 13:49:44 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-06-23 13:49:44 -0400 |
commit | de24f3e1b1a2264845779267d1fe3f406fc2cbf4 (patch) | |
tree | 27b28902cc8fa1aaae48f22f0d7ca8d9c2f127e7 | |
parent | 42b109c414aca7b1cf9069c1e1168a20dea3b343 (diff) | |
download | alembic-de24f3e1b1a2264845779267d1fe3f406fc2cbf4.tar.gz |
- Some deep-in-the-weeds fixes to try to get "server default" comparison
working better across platforms and expressions, in particular on
the Postgresql backend, mostly dealing with quoting/not quoting of various
expressions at the appropriate time and on a per-backend basis.
Repaired and tested support for such defaults as Postgresql interval
and array defaults.
fixes #212
-rw-r--r-- | alembic/autogenerate/compare.py | 11 | ||||
-rw-r--r-- | alembic/autogenerate/render.py | 12 | ||||
-rw-r--r-- | alembic/ddl/postgresql.py | 9 | ||||
-rw-r--r-- | alembic/ddl/sqlite.py | 9 | ||||
-rw-r--r-- | docs/build/changelog.rst | 11 | ||||
-rw-r--r-- | tests/test_postgresql.py | 105 |
6 files changed, 121 insertions, 36 deletions
diff --git a/alembic/autogenerate/compare.py b/alembic/autogenerate/compare.py index ec077fd..0d58bec 100644 --- a/alembic/autogenerate/compare.py +++ b/alembic/autogenerate/compare.py @@ -454,6 +454,12 @@ def _compare_type(schema, tname, cname, conn_col, conn_type, metadata_type, tname, cname ) +def _render_server_default_for_compare(metadata_default, + metadata_col, autogen_context): + return _render_server_default( + metadata_default, autogen_context, + repr_=metadata_col.type._type_affinity is sqltypes.String) + def _compare_server_default(schema, tname, cname, conn_col, metadata_col, diffs, autogen_context): @@ -461,8 +467,9 @@ def _compare_server_default(schema, tname, cname, conn_col, metadata_col, conn_col_default = conn_col.server_default if conn_col_default is None and metadata_default is None: return False - rendered_metadata_default = _render_server_default( - metadata_default, autogen_context) + rendered_metadata_default = _render_server_default_for_compare( + metadata_default, metadata_col, autogen_context) + rendered_conn_default = conn_col.server_default.arg.text \ if conn_col.server_default else None isdiff = autogen_context['context']._compare_server_default( diff --git a/alembic/autogenerate/render.py b/alembic/autogenerate/render.py index ed9536c..3828d87 100644 --- a/alembic/autogenerate/render.py +++ b/alembic/autogenerate/render.py @@ -307,7 +307,7 @@ def _render_column(column, autogen_context): 'kw': ", ".join(["%s=%s" % (kwname, val) for kwname, val in opts]) } -def _render_server_default(default, autogen_context): +def _render_server_default(default, autogen_context, repr_=True): rendered = _user_defined_render("server_default", default, autogen_context) if rendered is not False: return rendered @@ -319,11 +319,11 @@ def _render_server_default(default, autogen_context): default = str(default.arg.compile( dialect=autogen_context['dialect'])) if isinstance(default, string_types): - # TODO: this is just a hack to get - # tests to pass until we figure out - # WTF sqlite is doing - default = re.sub(r"^'|'$", "", default) - return repr(default) + if repr_: + default = re.sub(r"^'|'$", "", default) + return repr(default) + else: + return default else: return None diff --git a/alembic/ddl/postgresql.py b/alembic/ddl/postgresql.py index 5ca0d1f..27f31b0 100644 --- a/alembic/ddl/postgresql.py +++ b/alembic/ddl/postgresql.py @@ -1,7 +1,7 @@ import re from sqlalchemy import types as sqltypes - +from .. import compat from .base import compiles, alter_table, format_table_name, RenameTable from .impl import DefaultImpl @@ -24,8 +24,11 @@ class PostgresqlImpl(DefaultImpl): if None in (conn_col_default, rendered_metadata_default): return conn_col_default != rendered_metadata_default - if metadata_column.type._type_affinity is not sqltypes.String: - rendered_metadata_default = re.sub(r"^'|'$", "", rendered_metadata_default) + if metadata_column.server_default is not None and \ + isinstance(metadata_column.server_default.arg, + compat.string_types) and \ + not re.match(r"^'.+'$", rendered_metadata_default): + rendered_metadata_default = "'%s'" % rendered_metadata_default return not self.connection.scalar( "SELECT %s = %s" % ( diff --git a/alembic/ddl/sqlite.py b/alembic/ddl/sqlite.py index a3c73ce..85c829e 100644 --- a/alembic/ddl/sqlite.py +++ b/alembic/ddl/sqlite.py @@ -1,5 +1,6 @@ from .. import util from .impl import DefaultImpl +import re #from sqlalchemy.ext.compiler import compiles #from .base import AddColumn, alter_table @@ -29,6 +30,14 @@ class SQLiteImpl(DefaultImpl): raise NotImplementedError( "No support for ALTER of constraints in SQLite dialect") + def compare_server_default(self, inspector_column, + metadata_column, + rendered_metadata_default, + rendered_inspector_default): + + rendered_metadata_default = re.sub(r"^'|'$", "", rendered_metadata_default) + return rendered_inspector_default != repr(rendered_metadata_default) + def correct_for_autogen_constraints(self, conn_unique_constraints, conn_indexes, metadata_unique_constraints, metadata_indexes): diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 2d84283..33053e2 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -6,6 +6,17 @@ Changelog :version: 0.6.6 .. change:: + :tags: bug + :tickets: 212 + + Some deep-in-the-weeds fixes to try to get "server default" comparison + working better across platforms and expressions, in particular on + the Postgresql backend, mostly dealing with quoting/not quoting of various + expressions at the appropriate time and on a per-backend basis. + Repaired and tested support for such defaults as Postgresql interval + and array defaults. + + .. change:: :tags: enhancement :tickets: 209 diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index bd1c0b4..2e0965e 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -1,9 +1,13 @@ from unittest import TestCase -from sqlalchemy import DateTime, MetaData, Table, Column, text, Integer, String +from sqlalchemy import DateTime, MetaData, Table, Column, text, Integer, \ + String, Interval +from sqlalchemy.dialects.postgresql import ARRAY +from sqlalchemy.schema import DefaultClause from sqlalchemy.engine.reflection import Inspector from alembic.operations import Operations from sqlalchemy.sql import table, column +from alembic.autogenerate.compare import _compare_server_default from alembic import command, util from alembic.migration import MigrationContext @@ -158,7 +162,7 @@ class PostgresqlDefaultCompareTest(TestCase): 'connection': connection, 'dialect': connection.dialect, 'context': context - } + } @classmethod def teardown_class(cls): @@ -170,60 +174,111 @@ class PostgresqlDefaultCompareTest(TestCase): def tearDown(self): self.metadata.drop_all() - def _compare_default_roundtrip(self, type_, txt, alternate=None): - if alternate: - expected = True - else: - alternate = txt - expected = False - t = Table("test", self.metadata, - Column("somecol", type_, server_default=text(txt)) - ) + def _compare_default_roundtrip(self, type_, orig_default, alternate=None): + diff_expected = alternate is not None + if alternate is None: + alternate = orig_default + + t1 = Table("test", self.metadata, + Column("somecol", type_, server_default=orig_default)) t2 = Table("test", MetaData(), - Column("somecol", type_, server_default=text(alternate)) - ) - assert self._compare_default( - t, t2, t2.c.somecol, alternate - ) is expected + Column("somecol", type_, server_default=alternate)) + + t1.create(self.bind) + + insp = Inspector.from_engine(self.bind) + cols = insp.get_columns(t1.name) + insp_col = Column("somecol", cols[0]['type'], + server_default=text(cols[0]['default'])) + diffs = [] + _compare_server_default(None, "test", "somecol", insp_col, + t2.c.somecol, diffs, self.autogen_context) + eq_(bool(diffs), diff_expected) def _compare_default( self, t1, t2, col, rendered ): - t1.create(self.bind) + t1.create(self.bind, checkfirst=True) insp = Inspector.from_engine(self.bind) cols = insp.get_columns(t1.name) ctx = self.autogen_context['context'] + return ctx.impl.compare_server_default( None, col, rendered, cols[0]['default']) - def test_compare_current_timestamp(self): + def test_compare_interval_str(self): + # this form shouldn't be used but testing here + # for compatibility + self._compare_default_roundtrip( + Interval, + "14 days" + ) + + def test_compare_interval_text(self): + self._compare_default_roundtrip( + Interval, + text("'14 days'") + ) + + def test_compare_array_of_integer_text(self): + self._compare_default_roundtrip( + ARRAY(Integer), + text("(ARRAY[]::integer[])") + ) + + def test_compare_current_timestamp_text(self): self._compare_default_roundtrip( DateTime(), - "TIMEZONE('utc', CURRENT_TIMESTAMP)", + text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), ) - def test_compare_integer(self): + def test_compare_integer_str(self): self._compare_default_roundtrip( Integer(), "5", ) - def test_compare_integer_diff(self): + def test_compare_integer_text(self): self._compare_default_roundtrip( Integer(), - "5", "7" + text("5"), + ) + + def test_compare_integer_text_diff(self): + self._compare_default_roundtrip( + Integer(), + text("5"), "7" + ) + + def test_compare_character_str(self): + self._compare_default_roundtrip( + String(), + "hello", + ) + + def test_compare_character_text(self): + self._compare_default_roundtrip( + String(), + text("'hello'"), + ) + + def test_compare_character_str_diff(self): + self._compare_default_roundtrip( + String(), + "hello", + "there" ) - def test_compare_character_diff(self): + def test_compare_character_text_diff(self): self._compare_default_roundtrip( String(), - "'hello'", - "'there'" + text("'hello'"), + text("'there'") ) def test_primary_key_skip(self): |