Skip to content

Commit 96c3ba0

Browse files
committed
refactor(kernel): forward metadata filters unchanged
1 parent 765a95b commit 96c3ba0

6 files changed

Lines changed: 25 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Release History
22

33
# Unreleased
4-
- Kernel metadata filters now preserve empty strings, matching no catalogs, schemas, tables, or columns. Only `None` leaves a filter unset; other values retain their exact-identifier or pattern semantics (PECOBLR-4221).
4+
- Kernel metadata filters are now forwarded unchanged instead of collapsing empty strings to `None`. Only `None` leaves a filter unset; empty pattern filters match nothing (PECOBLR-4221).
55
- Kernel backend (`use_kernel=True`): OAuth **M2M with a JWT private-key client assertion** (RFC 7523) is now supported. Pass `oauth_client_id` + `oauth_jwt_key_file` + `oauth_jwt_kid` (with optional `oauth_jwt_passphrase` for an encrypted PKCS#8 key, `oauth_jwt_algorithm` defaulting to `RS256`, `oauth_scopes`, and `token_url` for the IdP token endpoint) and the connector routes them to the kernel's `auth_type="oauth-m2m-jwt"`, which signs a short-lived assertion with the private key instead of sending a client secret. The kernel owns the token lifecycle. A private-key file is treated as unambiguous JWT M2M intent and is mutually exclusive with `oauth_client_secret` / `credentials_provider` (both raise `NotSupportedError`). Verified end-to-end against an Azure Databricks workspace with the service principal's public certificate registered on its Entra ID app registration. Requires `databricks-sql-kernel >= 0.2.0` with JWT support.
66
- Kernel backend (`use_kernel=True`): OAuth U2M with `auth_type="databricks-oauth"` now forwards the connector's `databricks-sql-python` OAuth-app bundle (`client_id` + `sql offline_access` scopes + redirect port) into the kernel, so a bare U2M connection authenticates as `databricks-sql-python` — parity with the Thrift path — instead of inheriting the kernel's own `databricks-sql-connector` default. A caller-supplied `oauth_client_id` (with its coupled `oauth_redirect_port`) is honored, as is a caller-supplied `oauth_scopes`; absent one, the connector default (`sql offline_access`) is forwarded. Note: the kernel binds a single U2M redirect port, so unlike the Thrift path (which tries the full `8020..8024` range) the kernel path uses only one port and does not fall back to the next port if it is already bound — pass `oauth_redirect_port` (with `oauth_client_id`) to pick a free one on a port collision (PECOBLR-4040)
77
- Kernel backend (`use_kernel=True`): **Azure Entra (Azure AD) service-principal M2M is now supported.** `auth_type="azure-sp-m2m"` forwards `azure_client_id` / `azure_client_secret`; the kernel is the Azure-aware auth core — it builds the Entra v2.0 token endpoint and the `{app_id}/.default` scope, and **auto-discovers the tenant** from the workspace's `/aad/auth` redirect when `azure_tenant_id` is omitted (matching Thrift). The `Authorization` bearer is the Databricks-audience data token, which alone authenticates a workspace-member SP. Set `azure_workspace_resource_id` and the kernel also sends the Azure SP management token (`X-Databricks-Azure-SP-Management-Token`) + `X-Databricks-Azure-Workspace-Resource-Id` header (matching the JDBC driver), so a service principal with an Azure RBAC role but no workspace membership can authenticate; omit it and no ARM management-scope token is fetched. Azure AD **U2M** (`auth_type="azure-oauth"`) now routes to the kernel's OAuth U2M flow, identically to `auth_type="databricks-oauth"`: the kernel runs the in-house workspace-federated browser flow, which Azure workspaces support (the workspace federates login to Entra). It forwards the connector's `databricks-sql-python` OAuth app, not the Thrift Azure app (`96eecda7` / port 8030), which is registered for Thrift's direct-Entra flow the kernel does not perform (PECOBLR-4141; PECOBLR-4120)

src/databricks/sql/backend/databricks_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,8 @@ def get_schemas(
248248
max_rows: Maximum number of rows to fetch in a single batch
249249
max_bytes: Maximum number of bytes to fetch in a single batch
250250
cursor: The cursor object that will handle the results
251-
catalog_name: Optional exact catalog name to filter by. ``None``
252-
leaves the filter unset; an empty string matches nothing.
251+
catalog_name: Optional exact catalog name to filter by, forwarded
252+
unchanged. ``None`` leaves the filter unset.
253253
schema_name: Optional schema name pattern to filter by. ``None``
254254
leaves the filter unset; an empty string matches nothing.
255255
@@ -327,8 +327,8 @@ def get_columns(
327327
max_rows: Maximum number of rows to fetch in a single batch
328328
max_bytes: Maximum number of bytes to fetch in a single batch
329329
cursor: The cursor object that will handle the results
330-
catalog_name: Optional exact catalog name to filter by. ``None``
331-
leaves the filter unset; an empty string matches nothing.
330+
catalog_name: Optional exact catalog name to filter by, forwarded
331+
unchanged. ``None`` leaves the filter unset.
332332
schema_name: Optional schema name pattern to filter by. ``None``
333333
leaves the filter unset; an empty string matches nothing.
334334
table_name: Optional table name pattern to filter by

src/databricks/sql/backend/kernel/client.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import logging
2626
import threading
2727
import uuid
28-
from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING, Union
28+
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, Union
2929

3030
from databricks.sql.backend.databricks_client import DatabricksClient
3131
from databricks.sql.backend.kernel._errors import (
@@ -117,15 +117,6 @@ def _is_not_found(exc: BaseException) -> bool:
117117
)
118118

119119

120-
def _exact_catalog_and_pattern(
121-
catalog: Optional[str], pattern: Optional[str]
122-
) -> Tuple[Optional[str], Optional[str]]:
123-
"""Avoid constructing the kernel's invalid empty ``Identifier``."""
124-
if catalog == "":
125-
return None, ""
126-
return catalog, pattern
127-
128-
129120
def _is_staging_statement(operation: str) -> bool:
130121
"""True iff ``operation`` is a volume/staging statement (PUT / GET /
131122
REMOVE).
@@ -912,12 +903,9 @@ def get_schemas(
912903
if self._kernel_session is None:
913904
raise InterfaceError("get_schemas requires an open session.")
914905
try:
915-
catalog, schema_pattern = _exact_catalog_and_pattern(
916-
catalog_name, schema_name
917-
)
918906
stream = self._kernel_session.metadata().list_schemas(
919-
catalog=catalog,
920-
schema_pattern=schema_pattern,
907+
catalog=catalog_name,
908+
schema_pattern=schema_name,
921909
)
922910
return self._make_result_set(stream, cursor, self._synthetic_command_id())
923911
except Exception as exc:
@@ -972,12 +960,9 @@ def get_columns(
972960
# row's `TABLE_CAT` is correctly attributed. Matches the
973961
# Thrift backend's `getColumns(null, …)` behaviour from
974962
# the user's perspective.
975-
catalog, schema_pattern = _exact_catalog_and_pattern(
976-
catalog_name, schema_name
977-
)
978963
stream = self._kernel_session.metadata().list_columns(
979-
catalog=catalog,
980-
schema_pattern=schema_pattern,
964+
catalog=catalog_name,
965+
schema_pattern=schema_name,
981966
table_pattern=table_name,
982967
column_pattern=column_name,
983968
)

src/databricks/sql/client.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,8 +1577,9 @@ def schemas(
15771577
"""
15781578
Get schemas corresponding to the catalog_name and schema_name.
15791579
1580-
``None`` leaves a filter unset and an empty string matches nothing.
1581-
``catalog_name`` is exact; ``schema_name`` can contain % wildcards.
1580+
Filters are forwarded unchanged; only ``None`` leaves one unset.
1581+
``catalog_name`` is exact. ``schema_name`` is a pattern, can contain
1582+
% wildcards, and an empty pattern matches nothing.
15821583
:returns self
15831584
"""
15841585
self._check_not_closed()
@@ -1604,8 +1605,9 @@ def tables(
16041605
"""
16051606
Get tables corresponding to the catalog_name, schema_name and table_name.
16061607
1607-
``None`` leaves a filter unset and an empty string matches nothing.
1608-
Names can contain % wildcards.
1608+
Filters are forwarded unchanged; only ``None`` leaves one unset.
1609+
Names are patterns, can contain % wildcards, and empty patterns match
1610+
nothing.
16091611
:returns self
16101612
"""
16111613
self._check_not_closed()
@@ -1634,8 +1636,9 @@ def columns(
16341636
"""
16351637
Get columns corresponding to the catalog_name, schema_name, table_name and column_name.
16361638
1637-
``None`` leaves a filter unset and an empty string matches nothing.
1638-
``catalog_name`` is exact; other names can contain % wildcards.
1639+
Filters are forwarded unchanged; only ``None`` leaves one unset.
1640+
``catalog_name`` is exact. Other names are patterns, can contain
1641+
% wildcards, and empty patterns match nothing.
16391642
16401643
``catalog_name=None`` is accepted on all backends and matches
16411644
columns across every catalog (the kernel issues ``SHOW COLUMNS``

tests/e2e/test_kernel_backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def test_schemas_with_empty_string_filter_matches_nothing(conn):
367367

368368

369369
@pytest.mark.parametrize(
370-
"empty_filter", ["catalog_name", "schema_name", "table_name", "column_name"]
370+
"empty_filter", ["schema_name", "table_name", "column_name"]
371371
)
372372
def test_columns_with_empty_string_filter_matches_nothing(conn, empty_filter):
373373
filters = {

tests/unit/test_kernel_client.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,8 +1615,7 @@ def test_get_schemas_preserves_empty_pattern():
16151615
list_schemas.assert_called_once_with(catalog="main", schema_pattern="")
16161616

16171617

1618-
def test_get_schemas_empty_catalog_uses_empty_pattern():
1619-
"""Avoid passing an empty exact ``Identifier`` to the kernel."""
1618+
def test_get_schemas_preserves_empty_catalog():
16201619
c = _make_client()
16211620
c._kernel_session = MagicMock()
16221621
list_schemas = c._kernel_session.metadata.return_value.list_schemas
@@ -1634,7 +1633,7 @@ def test_get_schemas_empty_catalog_uses_empty_pattern():
16341633
schema_name="ignored",
16351634
)
16361635

1637-
list_schemas.assert_called_once_with(catalog=None, schema_pattern="")
1636+
list_schemas.assert_called_once_with(catalog="", schema_pattern="ignored")
16381637

16391638

16401639
def test_get_tables_preserves_empty_patterns():
@@ -1692,8 +1691,7 @@ def test_get_columns_preserves_empty_patterns():
16921691
)
16931692

16941693

1695-
def test_get_columns_empty_catalog_uses_empty_pattern():
1696-
"""An empty catalog matches nothing without constructing ``Identifier("")``."""
1694+
def test_get_columns_preserves_empty_catalog():
16971695
c = _make_client()
16981696
c._kernel_session = MagicMock()
16991697
list_columns = c._kernel_session.metadata.return_value.list_columns
@@ -1714,8 +1712,8 @@ def test_get_columns_empty_catalog_uses_empty_pattern():
17141712
)
17151713

17161714
list_columns.assert_called_once_with(
1717-
catalog=None,
1718-
schema_pattern="",
1715+
catalog="",
1716+
schema_pattern="ignored",
17191717
table_pattern="table",
17201718
column_pattern="column",
17211719
)

0 commit comments

Comments
 (0)