SameerMesiah97 commented on code in PR #74539:
URL: https://github.com/apache/airflow/pull/74539#discussion_r4238449931


##########
providers/microsoft/mssql/tests/unit/microsoft/mssql/hooks/test_mssql.py:
##########
@@ -381,3 +381,126 @@ def test_get_extra(self, get_connection, 
mssql_connections):
 
         hook = MsSqlHook()
         assert hook.get_connection().extra
+
+
+def _mssql_hook(extra: str = "{}") -> MsSqlHook:
+    conn = Connection(
+        conn_id="mssql_default",
+        conn_type="mssql",
+        host="localhost",
+        login="user",
+        password="secret",
+        schema="db",
+        port=1433,
+        extra=extra,
+    )
+    hook = MsSqlHook()
+    hook.get_connection = mock.Mock(return_value=conn)
+    return hook
+
+
+class TestMsSqlHookDbapiDriver:
+    def test_default_driver_is_pymssql(self):
+        hook = _mssql_hook()
+        assert hook.dbapi_driver == "pymssql"
+        assert hook.sqlalchemy_scheme == "mssql+pymssql"
+        assert hook.placeholder == "%s"
+
+    @pytest.mark.parametrize(
+        "extra",
+        [
+            '{"dbapi_driver": "mssql_python"}',
+            '{"DBAPI_Driver": "MSSQL_Python"}',
+        ],
+    )
+    def test_mssql_python_driver_selection(self, extra):
+        hook = _mssql_hook(extra)
+        assert hook.dbapi_driver == "mssql_python"
+        assert hook.sqlalchemy_scheme == "mssql+mssqlpython"
+
+    def test_unsupported_driver_raises(self):

Review Comment:
   I would extend this with parametrized cases for "", False, 0, a list and a 
dict in addition to "odbc".



##########
providers/microsoft/mssql/src/airflow/providers/microsoft/mssql/hooks/mssql.py:
##########
@@ -60,13 +83,39 @@ def __init__(
         self.schema = kwargs.pop("schema", None)
         self._sqlalchemy_scheme = sqlalchemy_scheme
 
+    @property
+    def dbapi_driver(self) -> str:
+        """DBAPI driver, either ``pymssql`` (default) or ``mssql_python``, 
from the connection extra."""
+        driver = str(self.connection_extra_lower.get("dbapi_driver") or 
self.DBAPI_DRIVER_PYMSSQL).lower()
+        if driver not in self.SUPPORTED_DBAPI_DRIVERS:
+            raise ValueError(
+                f"Unsupported dbapi_driver {driver!r} in the connection extra. 
"
+                f"Supported values: {', '.join(self.SUPPORTED_DBAPI_DRIVERS)}"
+            )
+        return driver

Review Comment:
   This could be cleaner. Please see below:
   
   ```
   driver = self.connection_extra_lower.get("dbapi_driver")
   if driver is None:
       return self.DBAPI_DRIVER_PYMSSQL
   
   if not isinstance(driver, str) or driver.lower() not in 
self.SUPPORTED_DBAPI_DRIVERS:
       raise ValueError(
           f"Unsupported dbapi_driver {driver!r} in the connection extra. "
           f"Supported values: {', 
'.join(sorted(self.SUPPORTED_DBAPI_DRIVERS))}"
       )
   
   return driver.lower()
   ```



##########
providers/microsoft/mssql/docs/connections/mssql.rst:
##########
@@ -74,3 +74,40 @@ If serializing with JSON:
         "port": 1433,
         "schema": "database_name"
     }'
+
+Choosing the DBAPI driver
+-------------------------
+
+By default the hook connects with `pymssql 
<https://pypi.org/project/pymssql/>`__. To use Microsoft's
+`mssql-python <https://pypi.org/project/mssql-python/>`__ driver instead, 
install the extra:
+
+.. code-block:: bash
+
+    pip install 'apache-airflow-providers-microsoft-mssql[mssql-python]'
+
+and set ``dbapi_driver`` to ``mssql_python`` in the connection extra:
+
+.. code-block:: bash
+
+    export AIRFLOW_CONN_MSSQL_DEFAULT='{
+        "conn_type": "mssql",
+        "login": "username",
+        "password": "password",
+        "host": "server.com",
+        "port": 1433,
+        "schema": "database_name",
+        "extra": {"dbapi_driver": "mssql_python", "TrustServerCertificate": 
"yes"}
+    }'
+
+Things to know when using ``mssql_python``:
+
+* ``dbapi_driver`` accepts ``pymssql`` (the default) or ``mssql_python``. The 
name is case-insensitive.
+* Every other extra is passed to ``mssql_python.connect`` as a connection 
string keyword, for example
+  ``Encrypt``, ``TrustServerCertificate`` or ``Authentication`` (for Microsoft 
Entra ID sign-in). The
+  host and port are sent as ``Server=host,port``.
+* The SQLAlchemy scheme defaults to ``mssql+mssqlpython``, which needs 
SQLAlchemy 2.1 or newer. It can still
+  be changed with the ``sqlalchemy_scheme`` extra or the hook argument.
+* The driver does not understand the plain ``%s`` parameter marker. Write SQL 
parameters as ``?`` with a
+  list or tuple, or as ``%(name)s`` with a dict. The hook's default 
placeholder for generated statements
+  is ``?`` and can be overridden with the ``placeholder`` extra.
+* ``mssql-python`` pools connections at the driver level by default.

Review Comment:
   I think this could be clearer and more concise. Please see the below:
   
   ```
   Notes
   ~~~~~
   
   * ``dbapi_driver`` accepts ``pymssql`` (the default) or ``mssql_python`` and 
is case-insensitive.
   * Additional driver options, such as ``Encrypt``, ``TrustServerCertificate`` 
and
     ``Authentication``, can be supplied through connection extras.
   * The default SQLAlchemy scheme is ``mssql+mssqlpython``, which requires 
SQLAlchemy 2.1
     or newer. Override it using ``sqlalchemy_scheme`` in the connection extra 
or hook argument.
   * Use ``?`` placeholders with positional parameters or ``%(name)s`` with 
named parameters.
     Plain ``%s`` placeholders are not supported. The hook defaults to ``?`` 
for generated statements.
   * Driver-level connection pooling is enabled by default.
   ```



##########
providers/microsoft/mssql/tests/unit/microsoft/mssql/hooks/test_mssql.py:
##########
@@ -381,3 +381,126 @@ def test_get_extra(self, get_connection, 
mssql_connections):
 
         hook = MsSqlHook()
         assert hook.get_connection().extra
+
+
+def _mssql_hook(extra: str = "{}") -> MsSqlHook:
+    conn = Connection(
+        conn_id="mssql_default",
+        conn_type="mssql",
+        host="localhost",
+        login="user",
+        password="secret",
+        schema="db",
+        port=1433,
+        extra=extra,
+    )
+    hook = MsSqlHook()
+    hook.get_connection = mock.Mock(return_value=conn)
+    return hook
+
+
+class TestMsSqlHookDbapiDriver:
+    def test_default_driver_is_pymssql(self):
+        hook = _mssql_hook()
+        assert hook.dbapi_driver == "pymssql"
+        assert hook.sqlalchemy_scheme == "mssql+pymssql"
+        assert hook.placeholder == "%s"
+
+    @pytest.mark.parametrize(
+        "extra",
+        [
+            '{"dbapi_driver": "mssql_python"}',
+            '{"DBAPI_Driver": "MSSQL_Python"}',
+        ],
+    )
+    def test_mssql_python_driver_selection(self, extra):
+        hook = _mssql_hook(extra)
+        assert hook.dbapi_driver == "mssql_python"
+        assert hook.sqlalchemy_scheme == "mssql+mssqlpython"
+
+    def test_unsupported_driver_raises(self):
+        hook = _mssql_hook('{"dbapi_driver": "odbc"}')
+        with pytest.raises(ValueError, match="Unsupported dbapi_driver"):
+            _ = hook.dbapi_driver
+
+    def test_sqlalchemy_scheme_extra_wins_over_driver_default(self):
+        hook = _mssql_hook('{"dbapi_driver": "mssql_python", 
"sqlalchemy_scheme": "mssql+pyodbc"}')
+        assert hook.sqlalchemy_scheme == "mssql+pyodbc"
+
+    def test_get_uri_strips_dbapi_driver(self):
+        hook = _mssql_hook('{"dbapi_driver": "mssql_python", 
"TrustServerCertificate": "yes"}')
+        uri = hook.get_uri()
+        assert 
uri.startswith("mssql+mssqlpython://user:secret@localhost:1433/db")
+        assert "dbapi_driver" not in uri
+        assert "TrustServerCertificate=yes" in uri
+
+    def test_mssql_python_placeholder_defaults_to_qmark(self):
+        assert _mssql_hook('{"dbapi_driver": "mssql_python"}').placeholder == 
"?"
+
+    def test_mssql_python_placeholder_extra_override(self):
+        hook = _mssql_hook('{"dbapi_driver": "mssql_python", "placeholder": 
"%s"}')
+        assert hook.placeholder == "%s"
+
+    @mock.patch("airflow.providers.microsoft.mssql.hooks.mssql.import_module")
+    def test_get_conn_mssql_python(self, mock_import_module):
+        extra = (
+            '{"dbapi_driver": "mssql_python", "sqlalchemy_scheme": 
"mssql+mssqlpython", '
+            '"placeholder": "?", "TrustServerCertificate": "yes", "Encrypt": 
""}'
+        )
+        hook = _mssql_hook(extra)
+        conn = hook.get_conn()
+        mock_import_module.assert_called_once_with("mssql_python")
+        mock_import_module.return_value.connect.assert_called_once_with(
+            Server="localhost,1433",
+            Database="db",
+            UID="user",
+            PWD="secret",
+            TrustServerCertificate="yes",
+        )
+        assert conn is mock_import_module.return_value.connect.return_value
+
+    @mock.patch(
+        "airflow.providers.microsoft.mssql.hooks.mssql.import_module",
+        side_effect=ImportError("no module"),
+    )
+    def test_get_conn_mssql_python_not_installed(self, mock_import_module):
+        hook = _mssql_hook('{"dbapi_driver": "mssql_python"}')
+        with pytest.raises(ImportError, match="mssql-python"):
+            hook.get_conn()
+
+    @mock.patch("pymssql.connect")
+    def test_get_conn_pymssql_ignores_hook_only_extras(self, mock_connect):
+        hook = _mssql_hook('{"dbapi_driver": "pymssql", "sqlalchemy_scheme": 
"mssql+pymssql", "timeout": 5}')
+        hook.get_conn()
+        mock_connect.assert_called_once_with(
+            server="localhost",
+            user="user",
+            password="secret",
+            database="db",
+            port="1433",
+            timeout=5,
+        )
+
+    def test_mssql_python_autocommit_uses_property(self):

Review Comment:
   Can you cover `autocommit=False` as well?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to