]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add pyodbc fast_executemany
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 11 Jul 2018 03:47:05 +0000 (23:47 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 11 Jul 2018 03:57:11 +0000 (23:57 -0400)
Added ``fast_executemany=True`` parameter to the SQL Server pyodbc dialect,
which enables use of pyodbc's new performance feature of the same name
when using Microsoft ODBC drivers.

Change-Id: I743fa7280e8f709addd330cfc7682623701cbb2e
Fixes: #4158
doc/build/changelog/migration_13.rst
doc/build/changelog/unreleased_13/4158.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mssql/pyodbc.py
test/dialect/mssql/test_engine.py
test/requirements.py

index 86187a2f4e8460f9119f737b2730acd3d926234b..9f2b71818a7002609ee5c20f7872534a6b5273f2 100644 (file)
@@ -200,3 +200,23 @@ Dialect Improvements and Changes - Oracle
 
 Dialect Improvements and Changes - SQL Server
 =============================================
+
+.. _change_4158:
+
+Support for pyodbc fast_executemany
+-----------------------------------
+
+Pyodbc's recently added "fast_executemany" mode, available when using the
+Microsoft ODBC driver, is now an option for the pyodbc / mssql dialect.
+Pass it via :func:`.create_engine`::
+
+    engine = create_engine(
+        "mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server",
+        fast_executemany=True)
+
+.. seealso::
+
+    :ref:`mssql_pyodbc_fastexecutemany`
+
+
+:ticket:`4158`
diff --git a/doc/build/changelog/unreleased_13/4158.rst b/doc/build/changelog/unreleased_13/4158.rst
new file mode 100644 (file)
index 0000000..ed1c159
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: feature, mssql
+    :tickets: 4158
+
+    Added ``fast_executemany=True`` parameter to the SQL Server pyodbc dialect,
+    which enables use of pyodbc's new performance feature of the same name
+    when using Microsoft ODBC drivers.
+
+    .. seealso::
+
+        :ref:`change_4158`
index 36bcc49b379c93ae0557838e3be2aec1a45551d5..34f81d6e8b687782817d61285de57ace79008300 100644 (file)
@@ -85,6 +85,33 @@ Pyodbc only has partial support for rowcount.  See the notes at
 :ref:`mssql_rowcount_versioning` for important notes when using ORM
 versioning.
 
+.. _mssql_pyodbc_fastexecutemany:
+
+Fast Executemany Mode
+---------------------
+
+The Pyodbc driver has added support for a "fast executemany" mode of execution
+which greatly reduces round trips for a DBAPI ``executemany()`` call when using
+Microsoft ODBC drivers.  The feature is enabled by setting the flag
+``.fast_executemany`` on the DBAPI cursor when an executemany call is to be
+used.   The SQLAlchemy pyodbc SQL Server dialect supports setting this flag
+automatically when the ``.fast_executemany`` flag is passed to
+:func:`.create_engine`; note that the ODBC driver must be the Microsoft driver
+in order to use this flag::
+
+    engine = create_engine(
+        "mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server",
+        fast_executemany=True)
+
+.. versionadded:: 1.3
+
+.. seealso::
+
+    `fast executemany
+    <https://github.com/mkleehammer/pyodbc/wiki/Features-beyond-the-DB-API#fast_executemany>`_
+    - on github
+
+
 """
 
 from .base import MSExecutionContext, MSDialect, BINARY, VARBINARY
@@ -264,7 +291,8 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect):
         }
     )
 
-    def __init__(self, description_encoding=None, **params):
+    def __init__(self, description_encoding=None, fast_executemany=False,
+                 **params):
         if 'description_encoding' in params:
             self.description_encoding = params.pop('description_encoding')
         super(MSDialect_pyodbc, self).__init__(**params)
@@ -273,6 +301,7 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect):
             hasattr(self.dbapi.Cursor, 'nextset')
         self._need_decimal_fix = self.dbapi and \
             self._dbapi_version() < (2, 1, 8)
+        self.fast_executemany = fast_executemany
 
     def _get_server_version_info(self, connection):
         try:
@@ -296,6 +325,12 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect):
                     pass
             return tuple(version)
 
+    def do_executemany(self, cursor, statement, parameters, context=None):
+        if self.fast_executemany:
+            cursor.fast_executemany = True
+        super(MSDialect_pyodbc, self).do_executemany(
+            cursor, statement, parameters, context=context)
+
     def is_disconnect(self, e, connection, cursor):
         if isinstance(e, self.dbapi.Error):
             for code in (
index 3c6c479f974a9071bd0fdcb05a7bac40caa5d658..973eb6dbbd53fbddf7465125642d2e0cade94517 100644 (file)
@@ -10,6 +10,8 @@ from sqlalchemy.testing import assert_raises_message, \
     assert_warnings, expect_warnings
 from sqlalchemy.testing.mock import Mock
 from sqlalchemy.dialects.mssql import base
+from sqlalchemy import Integer, String, Table, Column
+from sqlalchemy import event
 
 
 class ParseConnectTest(fixtures.TestBase):
@@ -257,6 +259,40 @@ class EngineFromConfigTest(fixtures.TestBase):
         eq_(e.dialect.legacy_schema_aliasing, False)
 
 
+class FastExecutemanyTest(fixtures.TestBase):
+    __only_on__ = 'mssql'
+    __backend__ = True
+    __requires__ = ('pyodbc_fast_executemany', )
+
+    @testing.provide_metadata
+    def test_flag_on(self):
+        t = Table(
+            't', self.metadata,
+            Column('id', Integer, primary_key=True),
+            Column('data', String(50))
+        )
+        t.create()
+
+        eng = engines.testing_engine(options={"fast_executemany": True})
+
+        @event.listens_for(eng, "after_cursor_execute")
+        def after_cursor_execute(
+                conn, cursor, statement, parameters, context, executemany):
+            if executemany:
+                assert cursor.fast_executemany
+
+        with eng.connect() as conn:
+            conn.execute(
+                t.insert(),
+                [{"id": i, "data": "data_%d" % i} for i in range(100)]
+            )
+
+            conn.execute(
+                t.insert(),
+                {"id": 200, "data": "data_200"}
+            )
+
+
 class VersionDetectionTest(fixtures.TestBase):
     def test_pymssql_version(self):
         dialect = pymssql.MSDialect_pymssql()
index b11a6317fd45bcf142f1649b717e5a7c7f76a5c2..f6ccbb6d8a217be26aeab12450e5c1251d4591f2 100644 (file)
@@ -1066,12 +1066,23 @@ class DefaultRequirements(SuiteRequirements):
             "works, but Oracle just gets tired with "
             "this much connection activity")
 
-
-
     @property
     def no_mssql_freetds(self):
         return self.mssql_freetds.not_()
 
+    @property
+    def pyodbc_fast_executemany(self):
+        def has_fastexecutemany(config):
+            if not against(config, "mssql+pyodbc"):
+                return False
+            with config.db.connect() as conn:
+                drivername = conn.connection.connection.getinfo(
+                    config.db.dialect.dbapi.SQL_DRIVER_NAME)
+                # on linux this is 'libmsodbcsql-13.1.so.9.2'.
+                # don't know what it is on windows
+                return "msodbc" in drivername
+        return only_if(has_fastexecutemany)
+
     @property
     def python_fixed_issue_8743(self):
         return exclusions.skip_if(