Skip to content

Commit 99e7bf7

Browse files
authored
Merge branch 'main' into fix/clickhouse-primary-key-paren
2 parents 2a1523b + b43743a commit 99e7bf7

7 files changed

Lines changed: 177 additions & 18 deletions

File tree

sqlmesh/core/config/connection.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from sys import version_info
1414

1515
import pydantic
16+
from pydantic import Field, computed_field
1617
from packaging import version
17-
from pydantic import Field
1818
from pydantic_core import from_json
1919
from sqlglot import exp
2020
from sqlglot.errors import ParseError
@@ -110,7 +110,14 @@ class ConnectionConfig(abc.ABC, BaseConfig):
110110
catalog_type_overrides: t.Optional[t.Dict[str, str]] = None
111111

112112
# Whether to share a single connection across threads or create a new connection per thread.
113-
shared_connection: t.ClassVar[bool] = False
113+
#
114+
# MyPy throws a "Decorators on top of @property are not supported" error despite this being a
115+
# valid decoration, and Pydantic recommend disabling the MyPy hint for this reason - see:
116+
# https://pydantic.dev/docs/validation/2.0/usage/computed_fields/
117+
@computed_field # type: ignore[prop-decorator]
118+
@property
119+
def shared_connection(self) -> bool:
120+
return False
114121

115122
@property
116123
@abc.abstractmethod
@@ -311,7 +318,10 @@ class BaseDuckDBConnectionConfig(ConnectionConfig):
311318

312319
token: t.Optional[str] = None
313320

314-
shared_connection: t.ClassVar[bool] = True
321+
@computed_field # type: ignore[prop-decorator]
322+
@property
323+
def shared_connection(self) -> bool:
324+
return True
315325

316326
_data_file_to_adapter: t.ClassVar[t.Dict[str, EngineAdapter]] = {}
317327

@@ -820,11 +830,15 @@ class DatabricksConnectionConfig(ConnectionConfig):
820830
DISPLAY_NAME: t.ClassVar[t.Literal["Databricks"]] = "Databricks"
821831
DISPLAY_ORDER: t.ClassVar[t.Literal[3]] = 3
822832

823-
shared_connection: t.ClassVar[bool] = True
824-
825833
_concurrent_tasks_validator = concurrent_tasks_validator
826834
_http_headers_validator = http_headers_validator
827835

836+
@computed_field # type: ignore[prop-decorator]
837+
@property
838+
def shared_connection(self) -> bool:
839+
"""The connection should only be shared if U2M OAuth is being used"""
840+
return self.auth_type is not None and self.oauth_client_secret is None
841+
828842
@model_validator(mode="before")
829843
def _databricks_connect_validator(cls, data: t.Any) -> t.Any:
830844
# SQLQueryContextLogger will output any error SQL queries even if they are in a try/except block.

sqlmesh/core/config/loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,4 +272,4 @@ def convert_config_type(
272272
config_obj: Config,
273273
config_type: t.Type[C],
274274
) -> C:
275-
return config_type.parse_obj(config_obj.dict())
275+
return config_type.parse_obj(config_obj.dict(exclude_computed_fields=True))

sqlmesh/core/engine_adapter/databricks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def _set_spark_engine_adapter_if_needed(self) -> None:
154154
host=self._extra_config["databricks_connect_server_hostname"],
155155
token=self._extra_config.get("databricks_connect_access_token"),
156156
)
157-
if "databricks_connect_use_serverless" in self._extra_config:
157+
if self._extra_config.get("databricks_connect_use_serverless"):
158158
connect_kwargs["serverless"] = True
159159
else:
160160
connect_kwargs["cluster_id"] = self._extra_config["databricks_connect_cluster_id"]

tests/core/engine_adapter/test_databricks.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,3 +732,108 @@ def test_columns(mocker: MockFixture, make_mocked_engine_adapter: t.Callable):
732732
adapter.cursor.execute.assert_called_once_with(
733733
"""SELECT columns.column_name, columns.full_data_type FROM system.information_schema.columns WHERE table_name = 'test_table' AND table_schema = 'test_db' AND table_catalog = 'test_catalog' ORDER BY ordinal_position ASC"""
734734
)
735+
736+
737+
def _make_databricks_connect_adapter(
738+
mocker: MockFixture,
739+
make_mocked_engine_adapter: t.Callable,
740+
extra_config: t.Dict[str, t.Any],
741+
) -> t.Tuple[DatabricksEngineAdapter, t.Any]:
742+
"""Helper that creates a DatabricksEngineAdapter with Databricks Connect mocked out."""
743+
import sys
744+
import types
745+
746+
mock_session = mocker.MagicMock()
747+
mock_builder = mocker.MagicMock()
748+
mock_builder.remote.return_value = mock_builder
749+
mock_builder.userAgent.return_value = mock_builder
750+
mock_builder.getOrCreate.return_value = mock_session
751+
mock_databricks_session_cls = mocker.MagicMock()
752+
mock_databricks_session_cls.builder = mock_builder
753+
754+
# databricks.connect is a local import inside the method, so inject via sys.modules
755+
mock_connect_module = types.ModuleType("databricks.connect")
756+
mock_connect_module.DatabricksSession = mock_databricks_session_cls # type: ignore
757+
mock_databricks_module = types.ModuleType("databricks")
758+
mocker.patch.dict(
759+
sys.modules,
760+
{"databricks": mock_databricks_module, "databricks.connect": mock_connect_module},
761+
)
762+
763+
mocker.patch(
764+
"sqlmesh.core.engine_adapter.databricks.DatabricksEngineAdapter.can_access_spark_session",
765+
return_value=False,
766+
)
767+
mocker.patch(
768+
"sqlmesh.core.engine_adapter.databricks.DatabricksEngineAdapter.can_access_databricks_connect",
769+
return_value=True,
770+
)
771+
772+
adapter = make_mocked_engine_adapter(
773+
DatabricksEngineAdapter,
774+
default_catalog="test_catalog",
775+
**extra_config,
776+
)
777+
return adapter, mock_builder
778+
779+
780+
def test_databricks_connect_routes_to_cluster_id(
781+
mocker: MockFixture, make_mocked_engine_adapter: t.Callable
782+
) -> None:
783+
"""cluster_id is used when databricks_connect_use_serverless is absent."""
784+
extra_config = {
785+
"databricks_connect_server_hostname": "myhost.azuredatabricks.net",
786+
"databricks_connect_access_token": "mytoken",
787+
"databricks_connect_cluster_id": "0123-456789-mycluster",
788+
}
789+
_, mock_builder = _make_databricks_connect_adapter(
790+
mocker, make_mocked_engine_adapter, extra_config
791+
)
792+
793+
mock_builder.remote.assert_called_once_with(
794+
host="myhost.azuredatabricks.net",
795+
token="mytoken",
796+
cluster_id="0123-456789-mycluster",
797+
)
798+
799+
800+
def test_databricks_connect_routes_to_serverless(
801+
mocker: MockFixture, make_mocked_engine_adapter: t.Callable
802+
) -> None:
803+
"""serverless=True is used when databricks_connect_use_serverless is truthy."""
804+
extra_config = {
805+
"databricks_connect_server_hostname": "myhost.azuredatabricks.net",
806+
"databricks_connect_access_token": "mytoken",
807+
"databricks_connect_cluster_id": "0123-456789-mycluster",
808+
"databricks_connect_use_serverless": True,
809+
}
810+
_, mock_builder = _make_databricks_connect_adapter(
811+
mocker, make_mocked_engine_adapter, extra_config
812+
)
813+
814+
mock_builder.remote.assert_called_once_with(
815+
host="myhost.azuredatabricks.net",
816+
token="mytoken",
817+
serverless=True,
818+
)
819+
820+
821+
def test_databricks_connect_cluster_id_not_overridden_by_falsy_serverless(
822+
mocker: MockFixture, make_mocked_engine_adapter: t.Callable
823+
) -> None:
824+
"""cluster_id is used when databricks_connect_use_serverless is present but False."""
825+
extra_config = {
826+
"databricks_connect_server_hostname": "myhost.azuredatabricks.net",
827+
"databricks_connect_access_token": "mytoken",
828+
"databricks_connect_cluster_id": "0123-456789-mycluster",
829+
"databricks_connect_use_serverless": False,
830+
}
831+
_, mock_builder = _make_databricks_connect_adapter(
832+
mocker, make_mocked_engine_adapter, extra_config
833+
)
834+
835+
mock_builder.remote.assert_called_once_with(
836+
host="myhost.azuredatabricks.net",
837+
token="mytoken",
838+
cluster_id="0123-456789-mycluster",
839+
)

tests/core/test_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ def test_connection_config_serialization():
532532
"extensions": [],
533533
"pre_ping": False,
534534
"pretty_sql": False,
535+
"shared_connection": True,
535536
"connector_config": {},
536537
"secrets": [],
537538
"filesystems": [],
@@ -544,6 +545,7 @@ def test_connection_config_serialization():
544545
"extensions": [],
545546
"pre_ping": False,
546547
"pretty_sql": False,
548+
"shared_connection": True,
547549
"connector_config": {},
548550
"secrets": [],
549551
"filesystems": [],

tests/core/test_connection_config.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,26 +1426,27 @@ def test_databricks(make_config):
14261426
)
14271427

14281428

1429-
def test_databricks_shared_connection(make_config):
1430-
"""Databricks should use a shared connection pool to prevent OAuth CSRF races.
1429+
def test_databricks__u2m_oauth__shared_connection_pool(make_config):
1430+
"""Databricks should use a shared connection pool when using OAuth to prevent CSRF races.
14311431
14321432
When concurrent_tasks > 1, ThreadLocalConnectionPool creates one connection per
14331433
thread. For U2M OAuth, each thread triggers its own browser-based OAuth flow;
14341434
these race on the CSRF state parameter and cause MismatchingStateError.
14351435
1436-
Setting shared_connection = True causes ThreadLocalSharedConnectionPool to be
1437-
used instead: a single connection is created (behind a lock) and each thread
1438-
gets its own cursor, so only one OAuth flow is ever initiated.
1436+
For non-U2M OAuth authentication types (e.g. access_token and M2M OAuth) then
1437+
ThreadLocalConnectionPool should still be used.
14391438
1440-
See: https://github.com/tobymao/sqlmesh/issues/5646
1439+
See:
1440+
https://github.com/tobymao/sqlmesh/issues/5646
1441+
https://github.com/SQLMesh/sqlmesh/issues/5858
14411442
"""
14421443
from sqlmesh.utils.connection_pool import ThreadLocalSharedConnectionPool
14431444

14441445
config = make_config(
14451446
type="databricks",
14461447
server_hostname="dbc-test.cloud.databricks.com",
14471448
http_path="sql/test/foo",
1448-
access_token="test-token",
1449+
auth_type="databricks-oauth",
14491450
concurrent_tasks=4,
14501451
)
14511452
assert isinstance(config, DatabricksConnectionConfig)
@@ -1455,6 +1456,43 @@ def test_databricks_shared_connection(make_config):
14551456
assert isinstance(adapter._connection_pool, ThreadLocalSharedConnectionPool)
14561457

14571458

1459+
@patch.object(DatabricksConnectionConfig, "_connection_factory_with_kwargs")
1460+
def test_databricks__m2m_oauth__connection_pool(mock_connection_factory_with_kwargs, make_config):
1461+
from sqlmesh.utils.connection_pool import ThreadLocalConnectionPool
1462+
1463+
config = make_config(
1464+
type="databricks",
1465+
server_hostname="dbc-test.cloud.databricks.com",
1466+
http_path="sql/test/foo",
1467+
auth_type="databricks-oauth",
1468+
oauth_client_id="oauth_client_id",
1469+
oauth_client_secret="oauth_client_secret",
1470+
concurrent_tasks=4,
1471+
)
1472+
assert isinstance(config, DatabricksConnectionConfig)
1473+
assert config.shared_connection is False
1474+
1475+
adapter = config.create_engine_adapter()
1476+
assert isinstance(adapter._connection_pool, ThreadLocalConnectionPool)
1477+
1478+
1479+
def test_databricks__access_token__connection_pool(make_config):
1480+
from sqlmesh.utils.connection_pool import ThreadLocalConnectionPool
1481+
1482+
config = make_config(
1483+
type="databricks",
1484+
server_hostname="dbc-test.cloud.databricks.com",
1485+
http_path="sql/test/foo",
1486+
access_token="any-token",
1487+
concurrent_tasks=4,
1488+
)
1489+
assert isinstance(config, DatabricksConnectionConfig)
1490+
assert config.shared_connection is False
1491+
1492+
adapter = config.create_engine_adapter()
1493+
assert isinstance(adapter._connection_pool, ThreadLocalConnectionPool)
1494+
1495+
14581496
def test_engine_import_validator():
14591497
with pytest.raises(
14601498
ConfigError,

tests/integrations/jupyter/test_magics.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -745,16 +745,16 @@ def test_info(notebook, sushi_context, convert_all_html_output_to_text, get_all_
745745
"Models: 20",
746746
"Macros: 8",
747747
"",
748-
"Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}\n secrets: None\n filesystems: []",
749-
"Test Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}\n secrets: None\n filesystems: []",
748+
"Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}\n secrets: None\n filesystems: []\n shared_connection: true",
749+
"Test Connection:\n type: duckdb\n concurrent_tasks: 1\n register_comments: true\n pre_ping: false\n pretty_sql: false\n extensions: []\n connector_config: {}\n secrets: None\n filesystems: []\n shared_connection: true",
750750
"Data warehouse connection succeeded",
751751
]
752752
assert get_all_html_output(output) == [
753753
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">Models: <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">20</span></pre>",
754754
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">Macros: <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">8</span></pre>",
755755
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"></pre>",
756-
'<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace">Connection: type: duckdb concurrent_tasks: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span> register_comments: true pre_ping: false pretty_sql: false extensions: <span style="font-weight: bold">[]</span> connector_config: <span style="font-weight: bold">{}</span> secrets: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span> filesystems: <span style="font-weight: bold">[]</span></pre>',
757-
'<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace">Test Connection: type: duckdb concurrent_tasks: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span> register_comments: true pre_ping: false pretty_sql: false extensions: <span style="font-weight: bold">[]</span> connector_config: <span style="font-weight: bold">{}</span> secrets: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span> filesystems: <span style="font-weight: bold">[]</span></pre>',
756+
'<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace">Connection: type: duckdb concurrent_tasks: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span> register_comments: true pre_ping: false pretty_sql: false extensions: <span style="font-weight: bold">[]</span> connector_config: <span style="font-weight: bold">{}</span> secrets: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span> filesystems: <span style="font-weight: bold">[]</span> shared_connection: true</pre>',
757+
'<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace">Test Connection: type: duckdb concurrent_tasks: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span> register_comments: true pre_ping: false pretty_sql: false extensions: <span style="font-weight: bold">[]</span> connector_config: <span style="font-weight: bold">{}</span> secrets: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span> filesystems: <span style="font-weight: bold">[]</span> shared_connection: true</pre>',
758758
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">Data warehouse connection <span style=\"color: #008000; text-decoration-color: #008000\">succeeded</span></pre>",
759759
]
760760

0 commit comments

Comments
 (0)