Skip to content

Commit e323e0e

Browse files
committed
fix(kernel): retain catalog wildcard normalization
1 parent 96c3ba0 commit e323e0e

5 files changed

Lines changed: 29 additions & 21 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 are now forwarded unchanged instead of collapsing empty strings to `None`. Only `None` leaves a filter unset; empty pattern filters match nothing (PECOBLR-4221).
4+
- Kernel metadata filters no longer collapse empty strings to `None`; empty patterns therefore match nothing. Existing `%`/`*` catalog wildcard handling is unchanged (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: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,9 @@ 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, forwarded
252-
unchanged. ``None`` leaves the filter unset.
251+
catalog_name: Optional exact catalog name. ``None`` leaves the
252+
filter unset; ``%`` and ``*`` select all catalogs; an empty
253+
string is preserved.
253254
schema_name: Optional schema name pattern to filter by. ``None``
254255
leaves the filter unset; an empty string matches nothing.
255256
@@ -327,8 +328,9 @@ def get_columns(
327328
max_rows: Maximum number of rows to fetch in a single batch
328329
max_bytes: Maximum number of bytes to fetch in a single batch
329330
cursor: The cursor object that will handle the results
330-
catalog_name: Optional exact catalog name to filter by, forwarded
331-
unchanged. ``None`` leaves the filter unset.
331+
catalog_name: Optional exact catalog name. ``None`` leaves the
332+
filter unset; ``%`` and ``*`` select all catalogs; an empty
333+
string is preserved.
332334
schema_name: Optional schema name pattern to filter by. ``None``
333335
leaves the filter unset; an empty string matches nothing.
334336
table_name: Optional table name pattern to filter by

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ def _is_not_found(exc: BaseException) -> bool:
117117
)
118118

119119

120+
def _catalog_or_none(value: Optional[str]) -> Optional[str]:
121+
"""Map supported all-catalog wildcards to the kernel's unset filter."""
122+
if value is None or value in ("%", "*"):
123+
return None
124+
return value
125+
126+
120127
def _is_staging_statement(operation: str) -> bool:
121128
"""True iff ``operation`` is a volume/staging statement (PUT / GET /
122129
REMOVE).
@@ -904,7 +911,7 @@ def get_schemas(
904911
raise InterfaceError("get_schemas requires an open session.")
905912
try:
906913
stream = self._kernel_session.metadata().list_schemas(
907-
catalog=catalog_name,
914+
catalog=_catalog_or_none(catalog_name),
908915
schema_pattern=schema_name,
909916
)
910917
return self._make_result_set(stream, cursor, self._synthetic_command_id())
@@ -931,7 +938,7 @@ def get_tables(
931938
# do the work — no connector-side drain + refilter. Passing it
932939
# through preserves streaming for large schemas.
933940
stream = self._kernel_session.metadata().list_tables(
934-
catalog=catalog_name,
941+
catalog=_catalog_or_none(catalog_name),
935942
schema_pattern=schema_name,
936943
table_pattern=table_name,
937944
table_types=table_types if table_types else None,
@@ -961,7 +968,7 @@ def get_columns(
961968
# Thrift backend's `getColumns(null, …)` behaviour from
962969
# the user's perspective.
963970
stream = self._kernel_session.metadata().list_columns(
964-
catalog=catalog_name,
971+
catalog=_catalog_or_none(catalog_name),
965972
schema_pattern=schema_name,
966973
table_pattern=table_name,
967974
column_pattern=column_name,

src/databricks/sql/client.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,9 +1577,9 @@ def schemas(
15771577
"""
15781578
Get schemas corresponding to the catalog_name and schema_name.
15791579
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.
1580+
Empty strings are preserved; ``None`` leaves a filter unset.
1581+
``catalog_name`` is exact except that % and * select all catalogs.
1582+
``schema_name`` is a pattern; an empty pattern matches nothing.
15831583
:returns self
15841584
"""
15851585
self._check_not_closed()
@@ -1605,9 +1605,8 @@ def tables(
16051605
"""
16061606
Get tables corresponding to the catalog_name, schema_name and table_name.
16071607
1608-
Filters are forwarded unchanged; only ``None`` leaves one unset.
1609-
Names are patterns, can contain % wildcards, and empty patterns match
1610-
nothing.
1608+
Empty strings are preserved; ``None`` leaves a filter unset. Names are
1609+
patterns, can contain % wildcards, and empty patterns match nothing.
16111610
:returns self
16121611
"""
16131612
self._check_not_closed()
@@ -1636,9 +1635,9 @@ def columns(
16361635
"""
16371636
Get columns corresponding to the catalog_name, schema_name, table_name and column_name.
16381637
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.
1638+
Empty strings are preserved; ``None`` leaves a filter unset.
1639+
``catalog_name`` is exact except that % and * select all catalogs.
1640+
Other names are patterns; empty patterns match nothing.
16421641
16431642
``catalog_name=None`` is accepted on all backends and matches
16441643
columns across every catalog (the kernel issues ``SHOW COLUMNS``

tests/unit/test_kernel_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,8 +1564,8 @@ def test_sync_execute_leaves_rowcount_default_when_num_modified_rows_none():
15641564
# ---------------------------------------------------------------------------
15651565

15661566

1567-
@pytest.mark.parametrize("exact_catalog", ["%", "*"])
1568-
def test_get_columns_preserves_exact_catalog(exact_catalog):
1567+
@pytest.mark.parametrize("catalog_wildcard", ["%", "*"])
1568+
def test_get_columns_normalizes_all_catalog_wildcard(catalog_wildcard):
15691569
c = _make_client()
15701570
c._kernel_session = MagicMock()
15711571
list_columns = c._kernel_session.metadata.return_value.list_columns
@@ -1579,14 +1579,14 @@ def test_get_columns_preserves_exact_catalog(exact_catalog):
15791579
max_rows=1,
15801580
max_bytes=1,
15811581
cursor=cursor,
1582-
catalog_name=exact_catalog,
1582+
catalog_name=catalog_wildcard,
15831583
schema_name="s",
15841584
table_name="t",
15851585
column_name="c",
15861586
)
15871587

15881588
list_columns.assert_called_once_with(
1589-
catalog=exact_catalog,
1589+
catalog=None,
15901590
schema_pattern="s",
15911591
table_pattern="t",
15921592
column_pattern="c",

0 commit comments

Comments
 (0)