]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
- Added a new option
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 7 Apr 2015 16:36:51 +0000 (12:36 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 7 Apr 2015 16:41:46 +0000 (12:41 -0400)
:paramref:`.EnvironmentContext.configure.literal_binds`, which
will pass the ``literal_binds`` flag into the compilation of SQL
constructs when using "offline" mode.  This has the effect that
SQL objects like inserts, updates, deletes as well as textual
statements sent using ``text()`` will be compiled such that the dialect
will attempt to render literal values "inline" automatically.
Only a subset of types is typically supported; the
:meth:`.Operations.inline_literal` construct remains as the construct
used to force a specific literal representation of a value.
The :paramref:`.EnvironmentContext.configure.literal_binds` flag
is added to the "offline" section of the ``env.py`` files generated
in new environments.
fixes #255
- enhance the op_fixture as well as MigrationContext._stdout_connection()
 so that it uses the real DefaultImpl
and MigrationContext fully in tests.

12 files changed:
alembic/ddl/impl.py
alembic/environment.py
alembic/migration.py
alembic/operations.py
alembic/templates/generic/env.py
alembic/templates/multidb/env.py
alembic/templates/pylons/env.py
alembic/testing/fixtures.py
docs/build/changelog.rst
tests/test_bulk_insert.py
tests/test_mysql.py
tests/test_op.py

index c91f1f1214464e3d9e1406b0a957e9302bd19418..176079c2db295d938bc3138589b86f13a516572b 100644 (file)
@@ -48,12 +48,22 @@ class DefaultImpl(with_metaclass(ImplMeta)):
         self.dialect = dialect
         self.connection = connection
         self.as_sql = as_sql
+        self.literal_binds = context_opts.get('literal_binds', False)
+        if self.literal_binds and not util.sqla_08:
+            util.warn("'literal_binds' flag not supported in SQLAlchemy 0.7")
+            self.literal_binds = False
+
         self.output_buffer = output_buffer
         self.memo = {}
         self.context_opts = context_opts
         if transactional_ddl is not None:
             self.transactional_ddl = transactional_ddl
 
+        if self.literal_binds:
+            if not self.as_sql:
+                raise util.CommandError(
+                    "Can't use literal_binds setting without as_sql mode")
+
     @classmethod
     def get_by_dialect(cls, dialect):
         return _impls[dialect.name]
@@ -95,8 +105,15 @@ class DefaultImpl(with_metaclass(ImplMeta)):
             if multiparams or params:
                 # TODO: coverage
                 raise Exception("Execution arguments not allowed with as_sql")
+
+            if self.literal_binds and not isinstance(
+                    construct, schema.DDLElement):
+                compile_kw = dict(compile_kwargs={"literal_binds": True})
+            else:
+                compile_kw = {}
+
             self.static_output(text_type(
-                construct.compile(dialect=self.dialect)
+                construct.compile(dialect=self.dialect, **compile_kw)
             ).replace("\t", "    ").strip() + self.command_terminator)
         else:
             conn = self.connection
index 45983d14e4508b12153fce6f93c18fd8800d6151..130a50fe9d1784ed3feee0da2bd15bbeabeb8fcd 100644 (file)
@@ -296,6 +296,7 @@ class EnvironmentContext(object):
                   compare_type=False,
                   compare_server_default=False,
                   render_item=None,
+                  literal_binds=False,
                   upgrade_token="upgrades",
                   downgrade_token="downgrades",
                   alembic_module_prefix="op.",
@@ -365,6 +366,24 @@ class EnvironmentContext(object):
          object.
         :param output_encoding: when using ``--sql`` to generate SQL
          scripts, apply this encoding to the string output.
+        :param literal_binds: when using ``--sql`` to generate SQL
+         scripts, pass through the ``literal_binds`` flag to the compiler
+         so that any literal values that would ordinarily be bound
+         parameters are converted to plain strings.
+
+         .. warning:: Dialects can typically only handle simple datatypes
+            like strings and numbers for auto-literal generation.  Datatypes
+            like dates, intervals, and others may still require manual
+            formatting, typically using :meth:`.Operations.inline_literal`.
+
+         .. note:: the ``literal_binds`` flag is ignored on SQLAlchemy
+            versions prior to 0.8 where this feature is not supported.
+
+         .. versionadded:: 0.7.6
+
+         .. seealso::
+
+            :meth:`.Operations.inline_literal`
 
         :param starting_rev: Override the "starting revision" argument
          when using ``--sql`` mode.
@@ -700,6 +719,7 @@ class EnvironmentContext(object):
         opts['sqlalchemy_module_prefix'] = sqlalchemy_module_prefix
         opts['alembic_module_prefix'] = alembic_module_prefix
         opts['user_module_prefix'] = user_module_prefix
+        opts['literal_binds'] = literal_binds
         if render_item is not None:
             opts['render_item'] = render_item
         if compare_type is not None:
index a2241fdcd5c0edfd4b16a2363f712bb5fc2a88da..9bd34edaa340795a96d75d9613688b5d09b899e1 100644 (file)
@@ -3,7 +3,7 @@ import sys
 from contextlib import contextmanager
 
 from sqlalchemy import MetaData, Table, Column, String, literal_column
-from sqlalchemy import create_engine
+from sqlalchemy.engine.strategies import MockEngineStrategy
 from sqlalchemy.engine import url as sqla_url
 
 from .compat import callable, EncodedIO
@@ -333,8 +333,7 @@ class MigrationContext(object):
         def dump(construct, *multiparams, **params):
             self.impl._exec(construct)
 
-        return create_engine("%s://" % self.dialect.name,
-                             strategy="mock", executor=dump)
+        return MockEngineStrategy.MockConnection(self.dialect, dump)
 
     @property
     def bind(self):
index 485943ee89c5bbbcada351e17ba5cf8f770fe431..83ccaa198ef26b818db59fe11484f4e019f912fb 100644 (file)
@@ -1197,6 +1197,12 @@ class Operations(object):
         See :meth:`.execute` for an example usage of
         :meth:`.inline_literal`.
 
+        The environment can also be configured to attempt to render
+        "literal" values inline automatically, for those simple types
+        that are supported by the dialect; see
+        :paramref:`.EnvironmentContext.configure.literal_binds` for this
+        more recently added feature.
+
         :param value: The value to render.  Strings, integers, and simple
          numerics should be supported.   Other types like boolean,
          dates, etc. may or may not be supported yet by various
@@ -1207,6 +1213,10 @@ class Operations(object):
          from the Python type of the value itself, as well as
          based on the context in which the value is used.
 
+        .. seealso::
+
+            :paramref:`.EnvironmentContext.configure.literal_binds`
+
         """
         return impl._literal_bindparam(None, value, type_=type_)
 
index 280006d5fc9b752d29ddf859acce0db5a8538e01..058378b9d037de152ec2ce6287b9cc6ea1eabe59 100644 (file)
@@ -36,7 +36,8 @@ def run_migrations_offline():
 
     """
     url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url, target_metadata=target_metadata)
+    context.configure(
+        url=url, target_metadata=target_metadata, literal_binds=True)
 
     with context.begin_transaction():
         context.run_migrations()
index ab371993aa13402b89e2ccaeba7ed51182639b23..453b41cc0a6984e8a281c38c09363e1624a1da86 100644 (file)
@@ -67,7 +67,8 @@ def run_migrations_offline():
         logger.info("Writing output to %s" % file_)
         with open(file_, 'w') as buffer:
             context.configure(url=rec['url'], output_buffer=buffer,
-                              target_metadata=target_metadata.get(name))
+                              target_metadata=target_metadata.get(name),
+                              literal_binds=True)
             with context.begin_transaction():
                 context.run_migrations(engine_name=name)
 
index 70eea4e0b8669f8d6e45fdc7fe264e0e979c750a..5ad9fd5958626a6c413522211822fee96b688c75 100644 (file)
@@ -46,7 +46,8 @@ def run_migrations_offline():
 
     """
     context.configure(
-        url=meta.engine.url, target_metadata=target_metadata)
+        url=meta.engine.url, target_metadata=target_metadata,
+        literal_binds=True)
     with context.begin_transaction():
         context.run_migrations()
 
index 4091388d2a94b5fae38600c0608fccfea984988c..ae25fd276827839eb0393eeed6bedd205860f07d 100644 (file)
@@ -88,39 +88,9 @@ def capture_context_buffer(**kw):
         yield buf
 
 
-def op_fixture(dialect='default', as_sql=False, naming_convention=None):
-    impl = _impls[dialect]
-
-    class Impl(impl):
-
-        def __init__(self, dialect, as_sql):
-            self.assertion = []
-            self.dialect = dialect
-            self.as_sql = as_sql
-            # TODO: this might need to
-            # be more like a real connection
-            # as tests get more involved
-            if as_sql and self.dialect.name != 'default':
-                # act similarly to MigrationContext
-                def dump(construct, *multiparams, **params):
-                    self._exec(construct)
-
-                self.connection = create_engine(
-                    "%s://" % self.dialect.name,
-                    strategy="mock", executor=dump)
-
-            else:
-                self.connection = mock.Mock(dialect=dialect)
-
-        def _exec(self, construct, *args, **kw):
-            if isinstance(construct, string_types):
-                construct = text(construct)
-            assert construct.supports_execution
-            sql = text_type(construct.compile(dialect=self.dialect))
-            sql = re.sub(r'[\n\t]', '', sql)
-            self.assertion.append(
-                sql
-            )
+def op_fixture(
+        dialect='default', as_sql=False,
+        naming_convention=None, literal_binds=False):
 
     opts = {}
     if naming_convention:
@@ -130,32 +100,67 @@ def op_fixture(dialect='default', as_sql=False, naming_convention=None):
                 "sqla 0.9.2 or greater")
         opts['target_metadata'] = MetaData(naming_convention=naming_convention)
 
-    class ctx(MigrationContext):
+    class buffer_(object):
+        def __init__(self):
+            self.lines = []
+
+        def write(self, msg):
+            msg = msg.strip()
+            msg = re.sub(r'[\n\t]', '', msg)
+            if as_sql:
+                # the impl produces soft tabs,
+                # so search for blocks of 4 spaces
+                msg = re.sub(r'    ', '', msg)
+                msg = re.sub('\;\n*$', '', msg)
+
+            self.lines.append(msg)
+
+        def flush(self):
+            pass
 
-        def __init__(self, dialect='default', as_sql=False):
-            self.dialect = _get_dialect(dialect)
-            self.impl = Impl(self.dialect, as_sql)
-            self.opts = opts
-            self.as_sql = as_sql
+    buf = buffer_()
 
+    class ctx(MigrationContext):
         def clear_assertions(self):
-            self.impl.assertion[:] = []
+            buf.lines[:] = []
 
         def assert_(self, *sql):
             # TODO: make this more flexible about
             # whitespace and such
-            eq_(self.impl.assertion, list(sql))
+            eq_(buf.lines, list(sql))
 
         def assert_contains(self, sql):
-            for stmt in self.impl.assertion:
+            for stmt in buf.lines:
                 if sql in stmt:
                     return
             else:
                 assert False, "Could not locate fragment %r in %r" % (
                     sql,
-                    self.impl.assertion
+                    buf.lines
                 )
-    context = ctx(dialect, as_sql)
+
+    if as_sql:
+        opts['as_sql'] = as_sql
+    if literal_binds:
+        opts['literal_binds'] = literal_binds
+    ctx_dialect = _get_dialect(dialect)
+    if not as_sql:
+        def execute(stmt, *multiparam, **param):
+            if isinstance(stmt, string_types):
+                stmt = text(stmt)
+            assert stmt.supports_execution
+            sql = text_type(stmt.compile(dialect=ctx_dialect))
+
+            buf.write(sql)
+
+        connection = mock.Mock(dialect=ctx_dialect, execute=execute)
+    else:
+        opts['output_buffer'] = buf
+        connection = None
+    context = ctx(
+        ctx_dialect,
+        connection,
+        opts)
+
     alembic.op._proxy = Operations(context)
     return context
-
index 9b27cc7214d447237e43f00a22a04a923c0bbbcc..dbfd3231e1a6ba61762f243f2c6c66e1cde3efab 100644 (file)
@@ -6,6 +6,24 @@ Changelog
 .. changelog::
     :version: 0.7.6
 
+    .. change::
+      :tags: feature, operations
+      :tickets: 255
+
+      Added a new option
+      :paramref:`.EnvironmentContext.configure.literal_binds`, which
+      will pass the ``literal_binds`` flag into the compilation of SQL
+      constructs when using "offline" mode.  This has the effect that
+      SQL objects like inserts, updates, deletes as well as textual
+      statements sent using ``text()`` will be compiled such that the dialect
+      will attempt to render literal values "inline" automatically.
+      Only a subset of types is typically supported; the
+      :meth:`.Operations.inline_literal` construct remains as the construct
+      used to force a specific literal representation of a value.
+      The :paramref:`.EnvironmentContext.configure.literal_binds` flag
+      is added to the "offline" section of the ``env.py`` files generated
+      in new environments.
+
     .. change::
       :tags: bug, batch
       :tickets: 289
index 5121e9072c95a3e1679977666a9db4644b30b1fb..26556302d3c9ad7dd337be342070c651a5236258 100644 (file)
@@ -165,15 +165,21 @@ class BulkInsertTest(TestBase):
         # doesn't have an IDENTITY column
         context.assert_(
             'SET IDENTITY_INSERT ins_table ON',
+            'GO',
             "INSERT INTO ins_table (id, v1, v2) "
             "VALUES (1, 'row v1', 'row v5')",
+            'GO',
             "INSERT INTO ins_table (id, v1, v2) "
             "VALUES (2, 'row v2', 'row v6')",
+            'GO',
             "INSERT INTO ins_table (id, v1, v2) "
             "VALUES (3, 'row v3', 'row v7')",
+            'GO',
             "INSERT INTO ins_table (id, v1, v2) "
             "VALUES (4, 'row v4', 'row v8')",
-            'SET IDENTITY_INSERT ins_table OFF'
+            'GO',
+            'SET IDENTITY_INSERT ins_table OFF',
+            'GO',
         )
 
     def test_bulk_insert_from_new_table(self):
index 7ce71a9720428e93bfbeaf3e56bd686b6f1141c6..2dc88387b2909f6b8ae5b3e7c3d9f1f580e58b78 100644 (file)
@@ -167,7 +167,7 @@ class MySQLOpTest(TestBase):
         context = op_fixture('mysql')
         op.drop_constraint('primary', 't1', type_='primary')
         context.assert_(
-            "ALTER TABLE t1 DROP PRIMARY KEY "
+            "ALTER TABLE t1 DROP PRIMARY KEY"
         )
 
     def test_drop_unique(self):
index 307ac42d5d084a4b3d8f165efb6201cb591c8c63..7d5f83e6d2aa5fce9a8942fd6ad5b9edf871c5d1 100644 (file)
@@ -819,3 +819,41 @@ class OpTest(TestBase):
         context = op_fixture('mssql')
         op.drop_index('ik_test', tablename='t1')
         context.assert_("DROP INDEX ik_test ON t1")
+
+
+class SQLModeOpTest(TestBase):
+    @config.requirements.sqlalchemy_09
+    def test_auto_literals(self):
+        context = op_fixture(as_sql=True, literal_binds=True)
+        from sqlalchemy.sql import table, column
+        from sqlalchemy import String, Integer
+
+        account = table('account',
+                        column('name', String),
+                        column('id', Integer)
+                        )
+        op.execute(
+            account.update().
+            where(account.c.name == op.inline_literal('account 1')).
+            values({'name': op.inline_literal('account 2')})
+        )
+        op.execute(text("update table set foo=:bar").bindparams(bar='bat'))
+        context.assert_(
+            "UPDATE account SET name='account 2' "
+            "WHERE account.name = 'account 1'",
+            "update table set foo='bat'"
+        )
+
+    def test_create_table_literal_binds(self):
+        context = op_fixture(as_sql=True, literal_binds=True)
+
+        op.create_table(
+            "some_table",
+            Column('id', Integer, primary_key=True),
+            Column('st_id', Integer, ForeignKey('some_table.id'))
+        )
+
+        context.assert_(
+            "CREATE TABLE some_table (id INTEGER NOT NULL, st_id INTEGER, "
+            "PRIMARY KEY (id), FOREIGN KEY(st_id) REFERENCES some_table (id))"
+        )
\ No newline at end of file