Skip to content

Commit

Permalink
- Fixed bug where :meth:.Operations.bulk_insert would not function
Browse files Browse the repository at this point in the history
properly when :meth:`.Operations.inline_literal` values were used,
either in --sql or non-sql mode.    The values will now render
directly in --sql mode.  For compatibility with "online" mode,
a new flag :paramref:`~.Operations.inline_literal.multiparams`
can be set to False which will cause each parameter set to be
compiled and executed with individual INSERT statements.
fixes #179
  • Loading branch information
zzzeek committed Mar 8, 2014
1 parent 334e04d commit 6cc9c97
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 8 deletions.
12 changes: 9 additions & 3 deletions alembic/ddl/impl.py
Expand Up @@ -163,23 +163,29 @@ def create_index(self, index):
def drop_index(self, index):
self._exec(schema.DropIndex(index))

def bulk_insert(self, table, rows):
def bulk_insert(self, table, rows, multiinsert=True):
if not isinstance(rows, list):
raise TypeError("List expected")
elif rows and not isinstance(rows[0], dict):
raise TypeError("List of dictionaries expected")
if self.as_sql:
for row in rows:
self._exec(table.insert(inline=True).values(**dict(
(k, _literal_bindparam(k, v, type_=table.c[k].type))
(k,
_literal_bindparam(k, v, type_=table.c[k].type)
if not isinstance(v, _literal_bindparam) else v)
for k, v in row.items()
)))
else:
# work around http://www.sqlalchemy.org/trac/ticket/2461
if not hasattr(table, '_autoincrement_column'):
table._autoincrement_column = None
if rows:
self._exec(table.insert(inline=True), multiparams=rows)
if multiinsert:
self._exec(table.insert(inline=True), multiparams=rows)
else:
for row in rows:
self._exec(table.insert(inline=True).values(**row))

def compare_type(self, inspector_column, metadata_column):

Expand Down
6 changes: 3 additions & 3 deletions alembic/ddl/mssql.py
Expand Up @@ -86,19 +86,19 @@ def alter_column(self, table_name, column_name,
schema=schema,
name=name)

def bulk_insert(self, table, rows):
def bulk_insert(self, table, rows, **kw):
if self.as_sql:
self._exec(
"SET IDENTITY_INSERT %s ON" %
self.dialect.identifier_preparer.format_table(table)
)
super(MSSQLImpl, self).bulk_insert(table, rows)
super(MSSQLImpl, self).bulk_insert(table, rows, **kw)
self._exec(
"SET IDENTITY_INSERT %s OFF" %
self.dialect.identifier_preparer.format_table(table)
)
else:
super(MSSQLImpl, self).bulk_insert(table, rows)
super(MSSQLImpl, self).bulk_insert(table, rows, **kw)


def drop_column(self, table_name, column, **kw):
Expand Down
51 changes: 49 additions & 2 deletions alembic/operations.py
Expand Up @@ -774,7 +774,7 @@ def drop_constraint(self, name, table_name, type_=None, schema=None):
t.append_constraint(const)
self.impl.drop_constraint(const)

def bulk_insert(self, table, rows):
def bulk_insert(self, table, rows, multiinsert=True):
"""Issue a "bulk insert" operation using the current
migration context.
Expand Down Expand Up @@ -808,8 +808,55 @@ def bulk_insert(self, table, rows):
'create_date':date(2008, 8, 15)},
]
)
When using --sql mode, some datatypes may not render inline automatically,
such as dates and other special types. When this issue is present,
:meth:`.Operations.inline_literal` may be used::
op.bulk_insert(accounts_table,
[
{'id':1, 'name':'John Smith',
'create_date':op.inline_literal("2010-10-05")},
{'id':2, 'name':'Ed Williams',
'create_date':op.inline_literal("2007-05-27")},
{'id':3, 'name':'Wendy Jones',
'create_date':op.inline_literal("2008-08-15")},
],
multiparams=False
)
When using :meth:`.Operations.inline_literal` in conjunction with
:meth:`.Operations.bulk_insert`, in order for the statement to work
in "online" (e.g. non --sql) mode, the
:paramref:`~.Operations.inline_literal.multiparams`
flag should be set to ``False``, which will have the effect of
individual INSERT statements being emitted to the database, each
with a distinct VALUES clause, so that the "inline" values can
still be rendered, rather than attempting to pass the values
as bound parameters.
.. versionadded:: 0.6.4 :meth:`.Operations.inline_literal` can now
be used with :meth:`.Operations.bulk_insert`, and the
:paramref:`~.Operations.inline_literal.multiparams` flag has
been added to assist in this usage when running in "online"
mode.
:param table: a table object which represents the target of the INSERT.
:param rows: a list of dictionaries indicating rows.
:param multiinsert: when at its default of True and --sql mode is not
enabled, the INSERT statement will be executed using
"executemany()" style, where all elements in the list of dictionaries
are passed as bound parameters in a single list. Setting this
to False results in individual INSERT statements being emitted
per parameter set, and is needed in those cases where non-literal
values are present in the parameter sets.
.. versionadded:: 0.6.4
"""
self.impl.bulk_insert(table, rows)
self.impl.bulk_insert(table, rows, multiinsert=multiinsert)

def inline_literal(self, value, type_=None):
"""Produce an 'inline literal' expression, suitable for
Expand Down
12 changes: 12 additions & 0 deletions docs/build/changelog.rst
Expand Up @@ -5,6 +5,18 @@ Changelog
.. changelog::
:version: 0.6.4

.. change::
:tags: bug
:tickets: 179

Fixed bug where :meth:`.Operations.bulk_insert` would not function
properly when :meth:`.Operations.inline_literal` values were used,
either in --sql or non-sql mode. The values will now render
directly in --sql mode. For compatibility with "online" mode,
a new flag :paramref:`~.Operations.inline_literal.multiparams`
can be set to False which will cause each parameter set to be
compiled and executed with individual INSERT statements.

.. change::
:tags: bug, py3k
:tickets: 175
Expand Down
38 changes: 38 additions & 0 deletions tests/test_bulk_insert.py
Expand Up @@ -4,6 +4,7 @@
from sqlalchemy import Integer, String
from sqlalchemy.sql import table, column
from sqlalchemy import Table, Column, MetaData
from sqlalchemy.types import TypeEngine

from . import op_fixture, eq_, assert_raises_message

Expand Down Expand Up @@ -108,6 +109,24 @@ def test_bulk_insert_mssql():
'INSERT INTO ins_table (id, v1, v2) VALUES (:id, :v1, :v2)'
)

def test_bulk_insert_inline_literal_as_sql():
context = op_fixture('postgresql', True)

class MyType(TypeEngine):
pass

t1 = table('t', column('id', Integer), column('data', MyType()))

op.bulk_insert(t1, [
{'id': 1, 'data': op.inline_literal('d1')},
{'id': 2, 'data': op.inline_literal('d2')},
])
context.assert_(
"INSERT INTO t (id, data) VALUES (1, 'd1')",
"INSERT INTO t (id, data) VALUES (2, 'd2')"
)


def test_bulk_insert_as_sql():
context = _test_bulk_insert('default', True)
context.assert_(
Expand Down Expand Up @@ -204,3 +223,22 @@ def test_bulk_insert_round_trip(self):
]
)

def test_bulk_insert_inline_literal(self):
class MyType(TypeEngine):
pass

t1 = table('foo', column('id', Integer), column('data', MyType()))

self.op.bulk_insert(t1, [
{'id': 1, 'data': self.op.inline_literal('d1')},
{'id': 2, 'data': self.op.inline_literal('d2')},
], multiinsert=False)

eq_(
self.conn.execute("select id, data from foo").fetchall(),
[
(1, "d1"),
(2, "d2"),
]
)

0 comments on commit 6cc9c97

Please sign in to comment.