]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
- add API support for inline literals
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 15 Nov 2011 22:17:27 +0000 (17:17 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 15 Nov 2011 22:17:27 +0000 (17:17 -0500)
- push ad-hoc table/column constructs for CRUD operations
- update docs to more comprehensively describe how to do
CRUD in migrations

alembic/op.py
tests/test_bulk_insert.py
tests/test_op.py

index cb016e88c9161eb82600867d856f1fbf6f4f5ec2..1732b40dc1997bd52e8ddc2ac87b29485d4fc03a 100644 (file)
@@ -1,4 +1,5 @@
 from alembic import util
+from alembic.ddl import impl
 from alembic.context import get_impl, get_context
 from sqlalchemy.types import NULLTYPE
 from sqlalchemy import schema, sql
@@ -14,6 +15,7 @@ __all__ = sorted([
             'drop_table',
             'drop_index',
             'create_index',
+            'inline_literal',
             'bulk_insert',
             'create_unique_constraint', 
             'get_context',
@@ -299,8 +301,16 @@ def bulk_insert(table, rows):
     
     e.g.::
     
-        from myapp.mymodel import accounts_table
         from datetime import date
+        from sqlalchemy.sql import table, column
+        from sqlalchemy import String, Integer, Date
+        
+        # Create an ad-hoc table to use for the insert statement.
+        accounts_table = table('account',
+            column('id', Integer),
+            column('name', String),
+            column('create_date', Date)
+        )
         
         bulk_insert(accounts_table,
             [
@@ -312,6 +322,40 @@ def bulk_insert(table, rows):
       """
     get_impl().bulk_insert(table, rows)
 
+def inline_literal(value, type_=None):
+    """Produce an 'inline literal' expression, suitable for 
+    using in an INSERT, UPDATE, or DELETE statement.
+    
+    When using Alembic in "offline" mode, CRUD operations
+    aren't compatible with SQLAlchemy's default behavior surrounding
+    literal values,
+    which is that they are converted into bound values and passed
+    separately into the ``execute()`` method of the DBAPI cursor.   
+    An offline SQL
+    script needs to have these rendered inline.  While it should
+    always be noted that inline literal values are an **enormous**
+    security hole in an application that handles untrusted input,
+    a schema migration is not run in this context, so 
+    literals are safe to render inline, with the caveat that
+    advanced types like dates may not be supported directly
+    by SQLAlchemy.
+
+    See :func:`.op.execute` for an example usage of
+    :func:`.inline_literal`.
+    
+    :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 
+     backends.
+    :param type_: optional - a :class:`sqlalchemy.types.TypeEngine` 
+     subclass stating the type of this value.  In SQLAlchemy 
+     expressions, this is usually derived automatically
+     from the Python type of the value itself, as well as
+     based on the context in which the value is used.
+
+    """
+    return impl._literal_bindparam(None, value, type_=type_)
+
 def execute(sql):
     """Execute the given SQL using the current change context.
     
@@ -326,24 +370,44 @@ def execute(sql):
         connection = get_bind()
     
     Also note that any parameterized statement here *will not work*
-    in offline mode - any kind of UPDATE or DELETE needs to render
-    inline expressions.   Due to these limitations, 
-    :func:`.execute` is overall not spectacularly useful for migration 
-    scripts that wish to run in offline mode.  Consider using the Alembic 
-    directives, or if the environment is only meant to run in 
-    "online" mode, use the ``get_context().bind``.
+    in offline mode - INSERT, UPDATE and DELETE statements which refer
+    to literal values would need to render
+    inline expressions.   For simple use cases, the :func:`.inline_literal`
+    function can be used for **rudimentary** quoting of string values.
+    For "bulk" inserts, consider using :func:`~alembic.op.bulk_insert`.
+    
+    For example, to emit an UPDATE statement which is equally
+    compatible with both online and offline mode::
+    
+        from sqlalchemy.sql import table, column
+        from sqlalchemy import String
+        from alembic.op import execute, inline_literal
+        
+        account = table('account', 
+            column('name', String)
+        )
+        execute(
+            account.update().\\
+                where(account.c.name==inline_literal('account 1')).\\
+                values({'name':inline_literal('account 2')})
+                )
+    
+    Note above we also used the SQLAlchemy :func:`sqlalchemy.sql.expression.table`
+    and :func:`sqlalchemy.sql.expression.column` constructs to make a brief,
+    ad-hoc table construct just for our UPDATE statement.  A full
+    :class:`~sqlalchemy.schema.Table` construct of course works perfectly
+    fine as well, though note it's a recommended practice to at least ensure
+    the definition of a table is self-contained within the migration script,
+    rather than imported from a module that may break compatibility with
+    older migrations.
     
     :param sql: Any legal SQLAlchemy expression, including:
     
     * a string
-    * a :func:`sqlalchemy.sql.expression.text` construct, with the caveat that
-      bound parameters won't work correctly in offline mode.
-    * a :func:`sqlalchemy.sql.expression.insert` construct.  If working 
-      in offline mode, consider using :func:`alembic.op.bulk_insert`
-      instead to support parameterization.
+    * a :func:`sqlalchemy.sql.expression.text` construct.
+    * a :func:`sqlalchemy.sql.expression.insert` construct.
     * a :func:`sqlalchemy.sql.expression.update`, :func:`sqlalchemy.sql.expression.insert`, 
-      or :func:`sqlalchemy.sql.expression.delete`  construct, with the caveat
-      that bound parameters won't work correctly in offline mode.
+      or :func:`sqlalchemy.sql.expression.delete`  construct.
     * Pretty much anything that's "executable" as described
       in :ref:`sqlexpression_toplevel`.
 
index be13602a35f76f1fc8a3dde47a7bf94b08821439..c39e8158f73617b71601f2db8688f6a1fb4d3eee 100644 (file)
@@ -1,15 +1,15 @@
 from tests import _op_fixture
 from alembic import op
-from sqlalchemy import Integer, Column, ForeignKey, \
-            UniqueConstraint, Table, MetaData, String
-from sqlalchemy.sql import table
+from sqlalchemy import Integer, \
+            UniqueConstraint, String
+from sqlalchemy.sql import table, column
 
 def _test_bulk_insert(dialect, as_sql):
     context = _op_fixture(dialect, as_sql)
     t1 = table("ins_table",
-                Column('id', Integer, primary_key=True),
-                Column('v1', String()),
-                Column('v2', String()),
+                column('id', Integer),
+                column('v1', String()),
+                column('v2', String()),
     )
     op.bulk_insert(t1, [
         {'id':1, 'v1':'row v1', 'v2':'row v5'},
@@ -27,10 +27,10 @@ def test_bulk_insert():
 
 def test_bulk_insert_wrong_cols():
     context = _op_fixture('postgresql')
-    t1 = Table("ins_table", MetaData(),
-                Column('id', Integer, primary_key=True),
-                Column('v1', String()),
-                Column('v2', String()),
+    t1 = table("ins_table", 
+                column('id', Integer),
+                column('v1', String()),
+                column('v2', String()),
     )
     op.bulk_insert(t1, [
         {'v1':'row v1', },
index d46f001d0609caf8ad0928d047353b5f72b62434..7503abf57ea7a939646175570d1e5af6f78d4ec3 100644 (file)
@@ -126,4 +126,26 @@ def test_create_table_two_fk():
             "FOREIGN KEY(foo_bar) REFERENCES foo (bar))"
     )
 
+def test_inline_literal():
+    context = _op_fixture()
+    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(
+        account.update().\
+            where(account.c.id==op.inline_literal(1)).\
+            values({'id':op.inline_literal(2)})
+            )
+    context.assert_(
+        "UPDATE account SET name='account 2' WHERE account.name = 'account 1'",
+        "UPDATE account SET id=2 WHERE account.id = 1"
+    )