databricks-sql-connector 4.3.0__tar.gz → 4.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/CHANGELOG.md +5 -0
  2. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/PKG-INFO +5 -8
  3. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/pyproject.toml +28 -17
  4. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/__init__.py +1 -1
  5. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/auth.py +6 -4
  6. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/authenticators.py +6 -8
  7. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/oauth.py +3 -3
  8. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/retry.py +1 -1
  9. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/thrift_http_client.py +5 -3
  10. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/token_federation.py +3 -3
  11. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/utils/filters.py +2 -4
  12. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/utils/http_client.py +5 -3
  13. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/thrift_backend.py +31 -11
  14. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/types.py +4 -0
  15. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/client.py +47 -11
  16. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/common/unified_http_client.py +7 -5
  17. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/exc.py +3 -1
  18. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/result_set.py +4 -0
  19. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/telemetry/telemetry_client.py +9 -9
  20. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/utils.py +18 -2
  21. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/LICENSE +0 -0
  22. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/README.md +0 -0
  23. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/__init__.py +0 -0
  24. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/__init__.py +0 -0
  25. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/auth_utils.py +0 -0
  26. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/common.py +0 -0
  27. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/endpoint.py +0 -0
  28. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/auth/oauth_http_handler.py +0 -0
  29. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/databricks_client.py +0 -0
  30. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/kernel/__init__.py +0 -0
  31. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/kernel/_errors.py +0 -0
  32. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/kernel/auth_bridge.py +0 -0
  33. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/kernel/client.py +0 -0
  34. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/kernel/result_set.py +0 -0
  35. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/kernel/type_mapping.py +0 -0
  36. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/backend.py +0 -0
  37. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/models/__init__.py +0 -0
  38. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/models/base.py +0 -0
  39. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/models/requests.py +0 -0
  40. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/models/responses.py +0 -0
  41. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/queue.py +0 -0
  42. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/result_set.py +0 -0
  43. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/utils/constants.py +0 -0
  44. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/utils/conversion.py +0 -0
  45. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/sea/utils/normalize.py +0 -0
  46. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/utils/__init__.py +0 -0
  47. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/backend/utils/guid_utils.py +0 -0
  48. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/cloudfetch/download_manager.py +0 -0
  49. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/cloudfetch/downloader.py +0 -0
  50. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/common/agent.py +0 -0
  51. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/common/feature_flag.py +0 -0
  52. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/common/http.py +0 -0
  53. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/common/http_utils.py +0 -0
  54. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/common/url_utils.py +0 -0
  55. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/experimental/__init__.py +0 -0
  56. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/experimental/oauth_persistence.py +0 -0
  57. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/parameters/__init__.py +0 -0
  58. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/parameters/native.py +0 -0
  59. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/parameters/py.typed +0 -0
  60. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/py.typed +0 -0
  61. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/session.py +0 -0
  62. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/telemetry/circuit_breaker_manager.py +0 -0
  63. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/telemetry/latency_logger.py +0 -0
  64. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/telemetry/models/endpoint_models.py +0 -0
  65. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/telemetry/models/enums.py +0 -0
  66. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/telemetry/models/event.py +0 -0
  67. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/telemetry/models/frontend_logs.py +0 -0
  68. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/telemetry/telemetry_push_client.py +0 -0
  69. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/telemetry/utils.py +0 -0
  70. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/thrift_api/TCLIService/TCLIService-remote +0 -0
  71. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/thrift_api/TCLIService/TCLIService.py +0 -0
  72. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/thrift_api/TCLIService/__init__.py +0 -0
  73. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/thrift_api/TCLIService/constants.py +0 -0
  74. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/thrift_api/TCLIService/ttypes.py +0 -0
  75. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/thrift_api/__init__.py +0 -0
  76. {databricks_sql_connector-4.3.0 → databricks_sql_connector-4.4.0}/src/databricks/sql/types.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Release History
2
2
 
3
+ # 4.4.0 (2026-07-22)
4
+ - Raised the minimum supported Python version to 3.10, dropping the end-of-life 3.8/3.9, to update the lockfile and clear CVE-flagged dependencies in the repo (databricks/databricks-sql-python#798)
5
+ - Fix: `REMOVE` staging operations no longer require `staging_allowed_local_path` to be set, since removing a remote file does not touch the local filesystem (databricks/databricks-sql-python#726)
6
+ - Report `cursor.rowcount` for DML on the Thrift backend: INSERT/UPDATE/DELETE/MERGE now set `rowcount` to the server's affected-row count instead of the hardcoded `-1`; SELECT (and statements the server does not report a count for) still return `-1`. `executemany` aggregates the count across all parameter sets per PEP 249 ([#784](https://github.com/databricks/databricks-sql-python/issues/784))
7
+
3
8
  # 4.3.0 (2026-06-12)
4
9
  - **New: optional Rust kernel backend (`use_kernel=True`).** Adds an alternative connection path backed by the native [`databricks-sql-kernel`](https://pypi.org/project/databricks-sql-kernel/) client (a Rust core exposed via PyO3), installable with the new `databricks-sql-connector[kernel]` extra. The kernel talks to Databricks over the **SEA (Statement Execution API) HTTP transport** — not Thrift — with CloudFetch and inline-Arrow result fetching, so `use_kernel=True` gives you a modern SEA-native client through the same DB-API surface. Supports PAT, OAuth M2M, and OAuth U2M auth. Requires Python >= 3.10 (the kernel wheel is `cp310-abi3`); on older interpreters the extra is a no-op and `use_kernel=True` raises a clear `ImportError`. The default backend remains Thrift — opt in per connection.
5
10
  - Kernel backend behavior is aligned with the Thrift backend so application code works the same either way: consistent cursor-state tracking (`query_id` / `get_query_state`), metadata (catalogs/schemas/tables/columns with JDBC-style filter semantics and case-insensitive `table_types`), DML `rowcount`, server-sourced async execution state, sync `cancel()`, fail-loud staging/volume operations, and structured error context (SQLSTATE, diagnostic info). Kernel logs surface through Python `logging` under the `databricks.sql.kernel` logger (databricks/databricks-sql-python#824, #825, #830, #838, #839 by @vikrantpuppala)
@@ -1,16 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-sql-connector
3
- Version: 4.3.0
3
+ Version: 4.4.0
4
4
  Summary: Databricks SQL Connector for Python
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
7
7
  Author: Databricks
8
8
  Author-email: databricks-sql-connector-maintainers@databricks.com
9
- Requires-Python: >=3.8.0,<4.0.0
9
+ Requires-Python: >=3.10,<4.0
10
10
  Classifier: License :: OSI Approved :: Apache Software License
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.8
13
- Classifier: Programming Language :: Python :: 3.9
14
12
  Classifier: Programming Language :: Python :: 3.10
15
13
  Classifier: Programming Language :: Python :: 3.11
16
14
  Classifier: Programming Language :: Python :: 3.12
@@ -19,21 +17,20 @@ Classifier: Programming Language :: Python :: 3.14
19
17
  Provides-Extra: kernel
20
18
  Provides-Extra: pyarrow
21
19
  Requires-Dist: databricks-sql-kernel (>=0.2.0,<0.3.0) ; (python_version >= "3.10") and (extra == "kernel")
22
- Requires-Dist: lz4 (>=4.0.2,<5.0.0) ; python_version >= "3.8" and python_version < "3.14"
20
+ Requires-Dist: lz4 (>=4.0.2,<5.0.0) ; python_version >= "3.10" and python_version < "3.14"
23
21
  Requires-Dist: lz4 (>=4.4.5,<5.0.0) ; python_version >= "3.14"
24
22
  Requires-Dist: oauthlib (>=3.1.0,<4.0.0)
25
23
  Requires-Dist: openpyxl (>=3.0.10,<4.0.0)
26
- Requires-Dist: pandas (>=1.2.5,<4.0.0) ; python_version >= "3.8" and python_version < "3.13"
24
+ Requires-Dist: pandas (>=1.2.5,<4.0.0) ; python_version >= "3.10" and python_version < "3.13"
27
25
  Requires-Dist: pandas (>=2.2.3,<4.0.0) ; python_version >= "3.13"
28
26
  Requires-Dist: pyarrow (>=14.0.1) ; (python_version >= "3.10" and python_version < "3.13") and (extra == "pyarrow")
29
- Requires-Dist: pyarrow (>=14.0.1,<23) ; (python_version >= "3.8" and python_version < "3.10") and (extra == "pyarrow")
30
27
  Requires-Dist: pyarrow (>=18.0.0) ; (python_version == "3.13") and (extra == "pyarrow")
31
28
  Requires-Dist: pyarrow (>=22.0.0) ; (python_version >= "3.14") and (extra == "pyarrow")
32
29
  Requires-Dist: pybreaker (>=1.0.0,<2.0.0)
33
30
  Requires-Dist: pyjwt (>=2.0.0,<3.0.0)
34
31
  Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
35
32
  Requires-Dist: requests (>=2.18.1,<3.0.0)
36
- Requires-Dist: thrift (>=0.22.0,<0.23.0)
33
+ Requires-Dist: thrift (>=0.24.0,<0.25.0)
37
34
  Requires-Dist: urllib3 (>=1.26)
38
35
  Project-URL: Bug Tracker, https://github.com/databricks/databricks-sql-python/issues
39
36
  Project-URL: Homepage, https://github.com/databricks/databricks-sql-python
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "databricks-sql-connector"
3
- version = "4.3.0"
3
+ version = "4.4.0"
4
4
  description = "Databricks SQL Connector for Python"
5
5
  authors = ["Databricks <databricks-sql-connector-maintainers@databricks.com>"]
6
6
  license = "Apache-2.0"
@@ -9,14 +9,28 @@ packages = [{ include = "databricks", from = "src" }]
9
9
  include = ["CHANGELOG.md"]
10
10
 
11
11
  [tool.poetry.dependencies]
12
- python = "^3.8.0"
13
- thrift = "~=0.22.0"
12
+ python = "^3.10"
13
+ # Floor is 0.24.0 -- the first release that both clears the open Apache Thrift
14
+ # CVEs (CVE-2025-48431 + the CVE-2026-41602..41636 set, all fixed in 0.23.0)
15
+ # AND is safe to install on DBR LTS. History: 0.23.0 fixed the CVEs but ships
16
+ # sdist-only and its setup.py calls sys.exit(0) on the build-success path,
17
+ # killing the PEP 517 backend on the OLD setuptools bundled by DBR LTS -- the
18
+ # SEV0 ES-1960554 (4.2.7 widened to <0.24.0 and was yanked; PR #840), which is
19
+ # why we held at ~=0.22.0. thrift 0.24.0 (THRIFT-6067) resolves this: it ships
20
+ # prebuilt manylinux2014 wheels (cp310-cp314) + macOS/musl/Windows, so pip uses
21
+ # a wheel and never runs setup.py on DBR LTS -- the build-time break cannot
22
+ # trigger. The `DBR LTS Install` CI check (.github/workflows/dbr-lts-install.yml)
23
+ # installs the built artifact on real DBR LTS clusters and is the authoritative
24
+ # gate for this. Cap at <0.25.0: thrift is pre-1.0, each 0.x minor can carry
25
+ # breaking changes or packaging regressions (see 0.23.0), so bump this
26
+ # deliberately once a new minor ships and the DBR-LTS gate proves it safe.
27
+ thrift = "~=0.24.0"
14
28
  pandas = [
15
- { version = ">=1.2.5,<4.0.0", python = ">=3.8,<3.13" },
29
+ { version = ">=1.2.5,<4.0.0", python = ">=3.10,<3.13" },
16
30
  { version = ">=2.2.3,<4.0.0", python = ">=3.13" }
17
31
  ]
18
32
  lz4 = [
19
- { version = "^4.0.2", python = ">=3.8,<3.14" },
33
+ { version = "^4.0.2", python = ">=3.10,<3.14" },
20
34
  { version = "^4.4.5", python = ">=3.14" }
21
35
  ]
22
36
  requests = "^2.18.1"
@@ -25,15 +39,6 @@ openpyxl = "^3.0.10"
25
39
  urllib3 = ">=1.26"
26
40
  python-dateutil = "^2.8.0"
27
41
  pyarrow = [
28
- # The <3.10 band is capped at <23 because pyarrow>=23 dropped
29
- # Python 3.9 (it requires >=3.10). Without the cap, poetry tries to
30
- # unify this entry with the kernel's transitive pyarrow>=23.0.1,<24
31
- # across the 3.8–3.10 slice and `poetry lock` fails ("pyarrow is
32
- # forbidden"). The cap removes no installable version — the newest
33
- # pyarrow with a 3.9 wheel is 21.x — it just makes that explicit to
34
- # the solver so the optional [kernel] extra (python>=3.10) can
35
- # coexist. See the kernel dep + [kernel] extra below.
36
- { version = ">=14.0.1,<23", python = ">=3.8,<3.10", optional=true },
37
42
  { version = ">=14.0.1", python = ">=3.10,<3.13", optional=true },
38
43
  { version = ">=18.0.0", python = ">=3.13,<3.14", optional=true },
39
44
  { version = ">=22.0.0", python = ">=3.14", optional=true }
@@ -77,15 +82,21 @@ pyarrow = ["pyarrow"]
77
82
  kernel = ["databricks-sql-kernel"]
78
83
 
79
84
  [tool.poetry.group.dev.dependencies]
80
- pytest = "^7.1.2"
85
+ pytest = "^9.0.3"
81
86
  mypy = "^1.10.1"
82
87
  pylint = ">=2.12.0"
83
- black = "^22.3.0"
88
+ black = "^26.3.1"
84
89
  pytest-dotenv = "^0.5.2"
85
90
  pytest-cov = "^4.0.0"
86
91
  pytest-xdist = "^3.0.0"
92
+ # tests/unit/test_parameters.py imports pytz directly. It used to arrive
93
+ # transitively via pandas, but pandas 3.0 (resolved on Python >=3.11)
94
+ # dropped pytz as a required dependency -- it is now only a pandas extra.
95
+ # Declare it explicitly here so the test suite does not depend on pandas's
96
+ # transitive graph, which differs across the Python matrix.
97
+ pytz = ">=2020.1"
87
98
  numpy = [
88
- { version = ">=1.16.6", python = ">=3.8,<3.11" },
99
+ { version = ">=1.16.6", python = ">=3.10,<3.11" },
89
100
  { version = ">=1.23.4", python = ">=3.11" },
90
101
  ]
91
102
 
@@ -71,7 +71,7 @@ DATETIME = DBAPITypeObject("timestamp")
71
71
  DATE = DBAPITypeObject("date")
72
72
  ROWID = DBAPITypeObject()
73
73
 
74
- __version__ = "4.3.0"
74
+ __version__ = "4.4.0"
75
75
  USER_AGENT_NAME = "PyDatabricksSqlConnector"
76
76
 
77
77
  # These two functions are pyhive legacy
@@ -102,7 +102,7 @@ def get_python_sql_connector_auth_provider(hostname: str, http_client, **kwargs)
102
102
  # TODO : unify all the auth mechanisms with the Python SDK
103
103
 
104
104
  auth_type = kwargs.get("auth_type")
105
- (client_id, redirect_port_range) = get_client_id_and_redirect_port(
105
+ client_id, redirect_port_range = get_client_id_and_redirect_port(
106
106
  auth_type == AuthType.AZURE_OAUTH.value
107
107
  )
108
108
 
@@ -124,9 +124,11 @@ def get_python_sql_connector_auth_provider(hostname: str, http_client, **kwargs)
124
124
  azure_client_secret=kwargs.get("azure_client_secret"),
125
125
  azure_tenant_id=kwargs.get("azure_tenant_id"),
126
126
  azure_workspace_resource_id=kwargs.get("azure_workspace_resource_id"),
127
- oauth_redirect_port_range=[kwargs["oauth_redirect_port"]]
128
- if kwargs.get("oauth_client_id") and kwargs.get("oauth_redirect_port")
129
- else redirect_port_range,
127
+ oauth_redirect_port_range=(
128
+ [kwargs["oauth_redirect_port"]]
129
+ if kwargs.get("oauth_client_id") and kwargs.get("oauth_redirect_port")
130
+ else redirect_port_range
131
+ ),
130
132
  oauth_persistence=kwargs.get("experimental_oauth_persistence"),
131
133
  credentials_provider=kwargs.get("credentials_provider"),
132
134
  identity_federation_client_id=kwargs.get("identity_federation_client_id"),
@@ -33,12 +33,10 @@ class CredentialsProvider(abc.ABC):
33
33
  for authenticating requests to Databricks REST APIs"""
34
34
 
35
35
  @abc.abstractmethod
36
- def auth_type(self) -> str:
37
- ...
36
+ def auth_type(self) -> str: ...
38
37
 
39
38
  @abc.abstractmethod
40
- def __call__(self, *args, **kwargs) -> HeaderFactory:
41
- ...
39
+ def __call__(self, *args, **kwargs) -> HeaderFactory: ...
42
40
 
43
41
 
44
42
  # Private API: this is an evolving interface and it will change in the future.
@@ -109,7 +107,7 @@ class DatabricksOAuthProvider(AuthProvider):
109
107
  if self._access_token and self._refresh_token:
110
108
  self._update_token_if_expired()
111
109
  else:
112
- (access_token, refresh_token) = self.oauth_manager.get_tokens(
110
+ access_token, refresh_token = self.oauth_manager.get_tokens(
113
111
  hostname=self._hostname, scope=self._scopes_as_str
114
112
  )
115
113
  self._access_token = access_token
@@ -231,9 +229,9 @@ class AzureServicePrincipalCredentialProvider(CredentialsProvider):
231
229
  }
232
230
 
233
231
  if self.azure_workspace_resource_id:
234
- headers[
235
- self.DATABRICKS_AZURE_WORKSPACE_RESOURCE_ID_HEADER
236
- ] = self.azure_workspace_resource_id
232
+ headers[self.DATABRICKS_AZURE_WORKSPACE_RESOURCE_ID_HEADER] = (
233
+ self.azure_workspace_resource_id
234
+ )
237
235
 
238
236
  return headers
239
237
 
@@ -45,7 +45,7 @@ class Token:
45
45
  exp_time = decoded_token.get("exp")
46
46
  current_time = time.time()
47
47
  buffer_time = 30 # 30 seconds buffer
48
- return exp_time and (exp_time - buffer_time) <= current_time
48
+ return exp_time is not None and (exp_time - buffer_time) <= current_time
49
49
  except Exception as e:
50
50
  logger.error("Failed to decode token: %s", e)
51
51
  raise e
@@ -134,7 +134,7 @@ class OAuthManager:
134
134
  try:
135
135
  with HTTPServer(("", port), handler) as httpd:
136
136
  redirect_url = OAuthManager.__get_redirect_url(port)
137
- (auth_req_uri, _, _) = client.prepare_authorization_request(
137
+ auth_req_uri, _, _ = client.prepare_authorization_request(
138
138
  authorization_url=auth_url,
139
139
  redirect_url=redirect_url,
140
140
  scope=scope,
@@ -269,7 +269,7 @@ class OAuthManager:
269
269
  auth_url = self.idp_endpoint.get_authorization_url(hostname)
270
270
 
271
271
  state = OAuthManager.__token_urlsafe(16)
272
- (verifier, challenge) = OAuthManager.__get_challenge()
272
+ verifier, challenge = OAuthManager.__get_challenge()
273
273
  client = oauthlib.oauth2.WebApplicationClient(self.client_id)
274
274
 
275
275
  try:
@@ -245,7 +245,7 @@ class DatabricksRetryPolicy(Retry):
245
245
  return self._command_type
246
246
 
247
247
  @command_type.setter
248
- def command_type(self, value: CommandType) -> None:
248
+ def command_type(self, value: Optional[CommandType]) -> None:
249
249
  self._command_type = value
250
250
 
251
251
  @property
@@ -122,9 +122,11 @@ class THttpClient(thrift.transport.THttpClient.THttpClient):
122
122
  pool_class = HTTPSConnectionPool
123
123
  _pool_kwargs.update(
124
124
  {
125
- "cert_reqs": ssl.CERT_REQUIRED
126
- if self._ssl_options.tls_verify
127
- else ssl.CERT_NONE,
125
+ "cert_reqs": (
126
+ ssl.CERT_REQUIRED
127
+ if self._ssl_options.tls_verify
128
+ else ssl.CERT_NONE
129
+ ),
128
130
  "ca_certs": self._ssl_options.tls_trusted_ca_file,
129
131
  "cert_file": self._ssl_options.tls_client_cert_file,
130
132
  "key_file": self._ssl_options.tls_client_cert_key_file,
@@ -111,9 +111,9 @@ class TokenFederationProvider(AuthProvider):
111
111
  """Add authentication headers to the request."""
112
112
 
113
113
  if self._cached_token and not self._cached_token.is_expired():
114
- request_headers[
115
- "Authorization"
116
- ] = f"{self._cached_token.token_type} {self._cached_token.access_token}"
114
+ request_headers["Authorization"] = (
115
+ f"{self._cached_token.token_type} {self._cached_token.access_token}"
116
+ )
117
117
  return
118
118
 
119
119
  # Get the external headers first to check if we need token federation
@@ -227,10 +227,8 @@ class ResultSetFilter:
227
227
  if not case_sensitive:
228
228
  allowed_values = [v.upper() for v in allowed_values]
229
229
  # Helper lambda to get column value based on case sensitivity
230
- get_column_value = (
231
- lambda row: row[column_index].upper()
232
- if not case_sensitive
233
- else row[column_index]
230
+ get_column_value = lambda row: (
231
+ row[column_index].upper() if not case_sensitive else row[column_index]
234
232
  )
235
233
 
236
234
  # Filter rows based on allowed values
@@ -161,9 +161,11 @@ class SeaHttpClient:
161
161
  pool_class = HTTPSConnectionPool
162
162
  pool_kwargs.update(
163
163
  {
164
- "cert_reqs": ssl.CERT_REQUIRED
165
- if self.ssl_options.tls_verify
166
- else ssl.CERT_NONE,
164
+ "cert_reqs": (
165
+ ssl.CERT_REQUIRED
166
+ if self.ssl_options.tls_verify
167
+ else ssl.CERT_NONE
168
+ ),
167
169
  "ca_certs": self.ssl_options.tls_trusted_ca_file,
168
170
  "cert_file": self.ssl_options.tls_client_cert_file,
169
171
  "key_file": self.ssl_options.tls_client_cert_key_file,
@@ -12,7 +12,6 @@ from databricks.sql.common.unified_http_client import UnifiedHttpClient
12
12
  from databricks.sql.result_set import ThriftResultSet
13
13
  from databricks.sql.telemetry.models.event import StatementType
14
14
 
15
-
16
15
  if TYPE_CHECKING:
17
16
  from databricks.sql.client import Cursor
18
17
  from databricks.sql.result_set import ResultSet
@@ -680,7 +679,10 @@ class ThriftDatabricksClient(DatabricksClient):
680
679
  num_rows,
681
680
  ) = convert_column_based_set_to_arrow_table(t_row_set.columns, description)
682
681
  elif t_row_set.arrowBatches is not None:
683
- (arrow_table, num_rows,) = convert_arrow_based_set_to_arrow_table(
682
+ (
683
+ arrow_table,
684
+ num_rows,
685
+ ) = convert_arrow_based_set_to_arrow_table(
684
686
  t_row_set.arrowBatches, lz4_compressed, schema_bytes
685
687
  )
686
688
  else:
@@ -801,7 +803,9 @@ class ThriftDatabricksClient(DatabricksClient):
801
803
  for col in t_table_schema.columns
802
804
  ]
803
805
 
804
- def _results_message_to_execute_response(self, resp, operation_state):
806
+ def _results_message_to_execute_response(
807
+ self, resp, operation_state, num_modified_rows=None
808
+ ):
805
809
  if resp.directResults and resp.directResults.resultSetMetadata:
806
810
  t_result_set_metadata_resp = resp.directResults.resultSetMetadata
807
811
  else:
@@ -864,6 +868,7 @@ class ThriftDatabricksClient(DatabricksClient):
864
868
  is_staging_operation=t_result_set_metadata_resp.isStagingOperation,
865
869
  arrow_schema_bytes=schema_bytes,
866
870
  result_format=t_result_set_metadata_resp.resultFormat,
871
+ num_modified_rows=num_modified_rows,
867
872
  )
868
873
 
869
874
  return execute_response, has_more_rows
@@ -945,6 +950,7 @@ class ThriftDatabricksClient(DatabricksClient):
945
950
  self._check_command_not_in_error_or_closed_state(
946
951
  op_handle, initial_operation_status_resp
947
952
  )
953
+ final_status_resp = initial_operation_status_resp
948
954
  operation_state = (
949
955
  initial_operation_status_resp
950
956
  and initial_operation_status_resp.operationState
@@ -956,7 +962,10 @@ class ThriftDatabricksClient(DatabricksClient):
956
962
  poll_resp = self._poll_for_status(op_handle)
957
963
  operation_state = poll_resp.operationState
958
964
  self._check_command_not_in_error_or_closed_state(op_handle, poll_resp)
959
- return operation_state
965
+ final_status_resp = poll_resp
966
+ # Return the terminal status response (not just the state) so callers
967
+ # can read ``numModifiedRows`` — the DML affected-row count — from it.
968
+ return operation_state, final_status_resp
960
969
 
961
970
  def get_query_state(self, command_id: CommandId) -> CommandState:
962
971
  thrift_handle = command_id.to_thrift_handle()
@@ -1046,11 +1055,13 @@ class ThriftDatabricksClient(DatabricksClient):
1046
1055
  statement=operation,
1047
1056
  runAsync=True,
1048
1057
  # For async operation we don't want the direct results
1049
- getDirectResults=None
1050
- if async_op
1051
- else ttypes.TSparkGetDirectResults(
1052
- maxRows=max_rows,
1053
- maxBytes=max_bytes,
1058
+ getDirectResults=(
1059
+ None
1060
+ if async_op
1061
+ else ttypes.TSparkGetDirectResults(
1062
+ maxRows=max_rows,
1063
+ maxBytes=max_bytes,
1064
+ )
1054
1065
  ),
1055
1066
  canReadArrowResult=True if pyarrow else False,
1056
1067
  canDecompressLZ4Result=lz4_compression,
@@ -1274,12 +1285,21 @@ class ThriftDatabricksClient(DatabricksClient):
1274
1285
  cursor.active_command_id = command_id
1275
1286
  self._check_direct_results_for_error(resp.directResults, self._host)
1276
1287
 
1277
- final_operation_state = self._wait_until_command_done(
1288
+ final_operation_state, final_status_resp = self._wait_until_command_done(
1278
1289
  resp.operationHandle,
1279
1290
  resp.directResults and resp.directResults.operationStatus,
1280
1291
  )
1281
1292
 
1282
- return self._results_message_to_execute_response(resp, final_operation_state)
1293
+ # ``numModifiedRows`` is populated by the server for DML statements
1294
+ # (INSERT/UPDATE/DELETE/MERGE) and is None for SELECT. Surface it so it
1295
+ # can flow to ``cursor.rowcount``.
1296
+ num_modified_rows = (
1297
+ final_status_resp.numModifiedRows if final_status_resp else None
1298
+ )
1299
+
1300
+ return self._results_message_to_execute_response(
1301
+ resp, final_operation_state, num_modified_rows
1302
+ )
1283
1303
 
1284
1304
  def _handle_execute_response_async(self, resp, cursor):
1285
1305
  command_id = CommandId.from_thrift_handle(resp.operationHandle)
@@ -425,3 +425,7 @@ class ExecuteResponse:
425
425
  is_staging_operation: bool = False
426
426
  arrow_schema_bytes: Optional[bytes] = None
427
427
  result_format: Optional[Any] = None
428
+ # Number of rows modified by a DML statement (INSERT/UPDATE/DELETE/MERGE),
429
+ # surfaced as ``cursor.rowcount``. ``None`` for SELECT and any statement
430
+ # for which the server does not report a count → ``rowcount`` stays at -1.
431
+ num_modified_rows: Optional[int] = None
@@ -340,9 +340,9 @@ class Connection:
340
340
  http_path=http_path,
341
341
  port=kwargs.get("_port", 443),
342
342
  client_context=client_context,
343
- user_agent=self.session.useragent_header
344
- if hasattr(self, "session")
345
- else None,
343
+ user_agent=(
344
+ self.session.useragent_header if hasattr(self, "session") else None
345
+ ),
346
346
  enable_telemetry=enable_telemetry,
347
347
  )
348
348
  raise e
@@ -390,9 +390,11 @@ class Connection:
390
390
 
391
391
  driver_connection_params = DriverConnectionParameters(
392
392
  http_path=http_path,
393
- mode=DatabricksClientType.SEA
394
- if self.session.use_sea
395
- else DatabricksClientType.THRIFT,
393
+ mode=(
394
+ DatabricksClientType.SEA
395
+ if self.session.use_sea
396
+ else DatabricksClientType.THRIFT
397
+ ),
396
398
  host_info=HostDetails(host_url=server_hostname, port=self.session.port),
397
399
  auth_mech=TelemetryHelper.get_auth_mechanism(self.session.auth_provider),
398
400
  auth_flow=TelemetryHelper.get_auth_flow(self.session.auth_provider),
@@ -874,7 +876,10 @@ class Cursor:
874
876
 
875
877
  self.connection: Connection = connection
876
878
 
877
- self.rowcount: int = -1 # Return -1 as this is not supported
879
+ # -1 until a statement runs. Set to the affected-row count after a DML
880
+ # statement (INSERT/UPDATE/DELETE/MERGE); stays -1 for SELECT and any
881
+ # statement the server does not report a modified-row count for.
882
+ self.rowcount: int = -1
878
883
  self.buffer_size_bytes: int = result_buffer_size_bytes
879
884
  self.active_result_set: Union[ResultSet, None] = None
880
885
  self.arraysize: int = arraysize
@@ -1039,6 +1044,9 @@ class Cursor:
1039
1044
  self.active_result_set.close()
1040
1045
  finally:
1041
1046
  self.active_result_set = None
1047
+ # Reset rowcount to its -1 default so a prior DML's count never
1048
+ # leaks into a subsequent SELECT (or unreported) statement.
1049
+ self.rowcount = -1
1042
1050
 
1043
1051
  def _check_not_closed(self):
1044
1052
  if not self.open:
@@ -1081,6 +1089,14 @@ class Cursor:
1081
1089
  headers=headers,
1082
1090
  )
1083
1091
 
1092
+ # REMOVE deletes a remote resource and never touches the local
1093
+ # filesystem, so it does not require staging_allowed_local_path.
1094
+ if row.operation == "REMOVE":
1095
+ return self._handle_staging_remove(
1096
+ presigned_url=row.presignedUrl,
1097
+ headers=headers,
1098
+ )
1099
+
1084
1100
  # For non-streaming operations, validate staging_allowed_local_path
1085
1101
  if isinstance(staging_allowed_local_path, type(str())):
1086
1102
  _staging_allowed_local_paths = [staging_allowed_local_path]
@@ -1133,14 +1149,12 @@ class Cursor:
1133
1149
  )
1134
1150
 
1135
1151
  # TODO: Create a retry loop here to re-attempt if the request times out or fails
1152
+ # REMOVE is handled above, before staging_allowed_local_path validation,
1153
+ # since it does not touch the local filesystem.
1136
1154
  if row.operation == "GET":
1137
1155
  return self._handle_staging_get(**handler_args)
1138
1156
  elif row.operation == "PUT":
1139
1157
  return self._handle_staging_put(**handler_args)
1140
- elif row.operation == "REMOVE":
1141
- # Local file isn't needed to remove a remote resource
1142
- handler_args.pop("local_file")
1143
- return self._handle_staging_remove(**handler_args)
1144
1158
  else:
1145
1159
  raise ProgrammingError(
1146
1160
  f"Operation {row.operation} is not supported. "
@@ -1367,6 +1381,14 @@ class Cursor:
1367
1381
  query_tags=query_tags,
1368
1382
  )
1369
1383
 
1384
+ # Surface the affected-row count for DML (INSERT/UPDATE/DELETE/MERGE) as
1385
+ # cursor.rowcount instead of the hardcoded -1. num_modified_rows is None
1386
+ # for SELECT (and statements the server does not report a count for) →
1387
+ # leave rowcount at its -1 default.
1388
+ num_modified_rows = getattr(self.active_result_set, "num_modified_rows", None)
1389
+ if num_modified_rows is not None:
1390
+ self.rowcount = num_modified_rows
1391
+
1370
1392
  if self.active_result_set and self.active_result_set.is_staging_operation:
1371
1393
  self._handle_staging_operation(
1372
1394
  staging_allowed_local_path=self.connection.staging_allowed_local_path,
@@ -1505,8 +1527,22 @@ class Cursor:
1505
1527
 
1506
1528
  :returns self
1507
1529
  """
1530
+ # Per PEP 249, rowcount after executemany reflects the total rows
1531
+ # affected across all parameter sets (or -1 when undeterminable). Each
1532
+ # execute() resets self.rowcount and sets it from its own statement, so
1533
+ # we accumulate the reported counts here. If no statement reports a
1534
+ # count (e.g. all SELECT, or the server does not report one), rowcount
1535
+ # stays at its -1 default.
1536
+ total_rowcount = -1
1508
1537
  for parameters in seq_of_parameters:
1509
1538
  self.execute(operation, parameters, query_tags=query_tags)
1539
+ if self.rowcount >= 0:
1540
+ total_rowcount = (
1541
+ self.rowcount
1542
+ if total_rowcount < 0
1543
+ else total_rowcount + self.rowcount
1544
+ )
1545
+ self.rowcount = total_rowcount
1510
1546
  return self
1511
1547
 
1512
1548
  @log_latency(StatementType.METADATA)
@@ -148,11 +148,13 @@ class UnifiedHttpClient:
148
148
  "num_pools": self.config.pool_connections,
149
149
  "maxsize": self.config.pool_maxsize,
150
150
  "retries": self._retry_policy,
151
- "timeout": urllib3.Timeout(
152
- connect=self.config.socket_timeout, read=self.config.socket_timeout
153
- )
154
- if self.config.socket_timeout
155
- else None,
151
+ "timeout": (
152
+ urllib3.Timeout(
153
+ connect=self.config.socket_timeout, read=self.config.socket_timeout
154
+ )
155
+ if self.config.socket_timeout
156
+ else None
157
+ ),
156
158
  "ssl_context": ssl_context,
157
159
  }
158
160
 
@@ -3,6 +3,7 @@ import logging
3
3
 
4
4
  logger = logging.getLogger(__name__)
5
5
 
6
+
6
7
  ### PEP-249 Mandated ###
7
8
  # https://peps.python.org/pep-0249/#exceptions
8
9
  class Error(Exception):
@@ -155,7 +156,8 @@ class CursorAlreadyClosedError(RequestError):
155
156
 
156
157
  class TelemetryRateLimitError(Exception):
157
158
  """Raised when telemetry endpoint returns 429 or 503, indicating rate limiting or service unavailable.
158
- This exception is used exclusively by the circuit breaker to track telemetry rate limiting events."""
159
+ This exception is used exclusively by the circuit breaker to track telemetry rate limiting events.
160
+ """
159
161
 
160
162
 
161
163
  class TelemetryNonRateLimitError(Exception):
@@ -50,6 +50,7 @@ class ResultSet(ABC):
50
50
  is_staging_operation: bool = False,
51
51
  lz4_compressed: bool = False,
52
52
  arrow_schema_bytes: Optional[bytes] = None,
53
+ num_modified_rows: Optional[int] = None,
53
54
  ):
54
55
  """
55
56
  A ResultSet manages the results of a single command.
@@ -82,6 +83,8 @@ class ResultSet(ABC):
82
83
  self._is_staging_operation = is_staging_operation
83
84
  self.lz4_compressed = lz4_compressed
84
85
  self._arrow_schema_bytes = arrow_schema_bytes
86
+ # Affected-row count for DML; None for SELECT / unreported.
87
+ self.num_modified_rows = num_modified_rows
85
88
 
86
89
  def __iter__(self):
87
90
  while True:
@@ -264,6 +267,7 @@ class ThriftResultSet(ResultSet):
264
267
  is_staging_operation=execute_response.is_staging_operation,
265
268
  lz4_compressed=execute_response.lz4_compressed,
266
269
  arrow_schema_bytes=execute_response.arrow_schema_bytes,
270
+ num_modified_rows=execute_response.num_modified_rows,
267
271
  )
268
272
 
269
273
  # Initialize results queue if not provided
@@ -490,9 +490,9 @@ class TelemetryClientFactory:
490
490
  on the telemetry endpoint.
491
491
  """
492
492
 
493
- _clients: Dict[
494
- str, _TelemetryClientHolder
495
- ] = {} # Map of host_url -> TelemetryClientHolder
493
+ _clients: Dict[str, _TelemetryClientHolder] = (
494
+ {}
495
+ ) # Map of host_url -> TelemetryClientHolder
496
496
  _executor: Optional[ThreadPoolExecutor] = None
497
497
  _initialized: bool = False
498
498
  _lock = threading.RLock() # Thread safety for factory operations
@@ -639,13 +639,13 @@ class TelemetryClientFactory:
639
639
  client_context=client_context,
640
640
  extra_headers=extra_headers,
641
641
  )
642
- TelemetryClientFactory._clients[
643
- host_url
644
- ] = _TelemetryClientHolder(client)
642
+ TelemetryClientFactory._clients[host_url] = (
643
+ _TelemetryClientHolder(client)
644
+ )
645
645
  else:
646
- TelemetryClientFactory._clients[
647
- host_url
648
- ] = _TelemetryClientHolder(NoopTelemetryClient())
646
+ TelemetryClientFactory._clients[host_url] = (
647
+ _TelemetryClientHolder(NoopTelemetryClient())
648
+ )
649
649
  except Exception as e:
650
650
  logger.debug("Failed to initialize telemetry client: %s", e)
651
651
  # Fallback to NoopTelemetryClient to ensure connection doesn't fail
@@ -876,7 +876,7 @@ def _create_python_tuple(t_col_value_wrapper):
876
876
 
877
877
 
878
878
  def concat_table_chunks(
879
- table_chunks: List[Union["pyarrow.Table", ColumnTable]]
879
+ table_chunks: List[Union["pyarrow.Table", ColumnTable]],
880
880
  ) -> Union["pyarrow.Table", ColumnTable]:
881
881
  if len(table_chunks) == 0:
882
882
  return table_chunks
@@ -894,11 +894,27 @@ def concat_table_chunks(
894
894
  result_table[j].extend(table_chunks[i].column_table[j])
895
895
  return ColumnTable(result_table, table_chunks[0].column_names)
896
896
  else:
897
+ return _concat_arrow_tables(table_chunks)
898
+
899
+
900
+ def _concat_arrow_tables(table_chunks: List["pyarrow.Table"]) -> "pyarrow.Table":
901
+ """Concatenate Arrow tables, tolerant of the installed pyarrow version.
902
+
903
+ ``promote_options`` was added in pyarrow 14.0.0, replacing the older
904
+ ``promote`` boolean. The connector's declared floor is pyarrow>=14, but a
905
+ base install (no ``[pyarrow]`` extra) can run against a runtime's older
906
+ bundled pyarrow -- e.g. DBR 13.3 (12.x) / 14.3 -- where ``promote_options``
907
+ raises ``TypeError: unexpected keyword argument``. Fall back to the legacy
908
+ ``promote=True`` there (equivalent to ``promote_options="default"``).
909
+ """
910
+ try:
897
911
  return pyarrow.concat_tables(table_chunks, promote_options="default")
912
+ except TypeError:
913
+ return pyarrow.concat_tables(table_chunks, promote=True)
898
914
 
899
915
 
900
916
  def serialize_query_tags(
901
- query_tags: Optional[Dict[str, Optional[str]]]
917
+ query_tags: Optional[Dict[str, Optional[str]]],
902
918
  ) -> Optional[str]:
903
919
  """
904
920
  Serialize query_tags dictionary to a string format.