Skip to content

Commit 761193b

Browse files
feat(kernel): wire statement query tags + http_headers/user_agent on use_kernel
Consume the kernel surface from kernel PR (query tags + custom HTTP headers) on the use_kernel path. Bumps KERNEL_REV. Query tags (statement-level): - execute_command no longer raises NotSupportedError for query_tags; it calls stmt.set_query_tags(query_tags) after set_sql. The connector already passes Dict[str, Optional[str]], which the kernel accepts (None value -> bare key in the SEA query_tags conf). http_headers + user_agent_entry: - The kernel client now forwards http_headers to the kernel Session as the `http_headers` kwarg (was accept-and-ignore). session.py already passes all_headers, which carries the connector's composed User-Agent (PyDatabricksSqlConnector/x (entry)) + caller headers + SPOG org-id. - The kernel applies them per request: its own Authorization / org-id win; a caller User-Agent is APPENDED to the kernel base UA (the base carries the DatabricksJDBCDriverOSS token that gates the SEA result disposition, so it's never replaced). So user_agent_entry is honored end-to-end via the existing http_headers forwarding — no separate kwarg needed. Tests: - unit: query_tags forwarded to set_query_tags (was: rejection test); http_headers forwarded to the kernel Session (and omitted when empty). - e2e (test_kernel_backend.py): a query_tagged query and a connection with user_agent_entry + a custom http_header both round-trip green against a dogfood warehouse. The UA case specifically guards the append behavior (replacing the base UA would 400 on the result disposition). KERNEL_REV -> c2053f68b75fef4a29425096dc6bbafb774d8b83 (kernel PR #119 branch HEAD; re-pin to the squash-merge SHA once #119 lands). 194 kernel unit tests pass; black + mypy clean; 3 e2e pass live. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
1 parent 91cd0a6 commit 761193b

4 files changed

Lines changed: 133 additions & 23 deletions

File tree

KERNEL_REV

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
b4d88220cdfad8dba1cfa89892269342ae26feeb
1+
c2053f68b75fef4a29425096dc6bbafb774d8b83

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

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,19 @@ def __init__(
9191
):
9292
# ``ssl_options`` is translated to the kernel's ``tls_*``
9393
# Session kwargs in ``open_session`` (custom CA, verify
94-
# toggles, mTLS client cert/key). ``http_headers`` /
95-
# ``http_client`` / ``port`` are still accept-and-ignore — the
96-
# kernel manages its own HTTP stack.
94+
# toggles, mTLS client cert/key). ``http_headers`` is forwarded
95+
# to the kernel as custom request headers (it carries the
96+
# connector's composed ``User-Agent`` + any caller headers + the
97+
# SPOG ``x-databricks-org-id``). ``http_client`` / ``port`` are
98+
# still accept-and-ignore — the kernel manages its own HTTP
99+
# stack.
97100
self._server_hostname = server_hostname
98101
self._http_path = http_path
99102
self._auth_provider = auth_provider
100103
self._ssl_options = ssl_options
104+
# Caller / connector HTTP headers (list of (name, value) pairs).
105+
# Forwarded to the kernel Session in ``open_session``.
106+
self._http_headers = http_headers or []
101107
# Raw auth-relevant connect() kwargs (auth_type,
102108
# oauth_client_id/secret, redirect port, credentials_provider).
103109
# The kernel auth bridge needs these to build OAuth kwargs — the
@@ -187,6 +193,15 @@ def open_session(
187193
# Translate the connector's ``_retry_*`` kwargs into the kernel's
188194
# ``retry_*`` Session kwargs. Empty when retry is left at defaults.
189195
retry_kwargs = _kernel_retry_kwargs(self._retry_options)
196+
# Forward caller / connector HTTP headers. The kernel applies
197+
# them on every request (its own Authorization / org-id win); a
198+
# caller ``User-Agent`` is appended to the kernel's base UA. Only
199+
# pass the kwarg when there's something to send.
200+
http_headers_kwargs: Dict[str, Any] = {}
201+
if self._http_headers:
202+
http_headers_kwargs["http_headers"] = [
203+
(str(k), str(v)) for k, v in self._http_headers
204+
]
190205
try:
191206
self._kernel_session = _kernel.Session(
192207
host=self._server_hostname,
@@ -208,6 +223,7 @@ def open_session(
208223
**auth_kwargs,
209224
**tls_kwargs,
210225
**retry_kwargs,
226+
**http_headers_kwargs,
211227
)
212228
except Exception as exc:
213229
raise _wrap_kernel_exception("open_session", exc) from exc
@@ -304,10 +320,6 @@ def execute_command(
304320
) -> Union["ResultSet", None]:
305321
if self._kernel_session is None:
306322
raise InterfaceError("Cannot execute_command without an open session.")
307-
if query_tags:
308-
raise NotSupportedError(
309-
"Statement-level query_tags are not yet supported on the kernel backend."
310-
)
311323

312324
try:
313325
stmt = self._kernel_session.statement()
@@ -321,6 +333,13 @@ def execute_command(
321333
try:
322334
try:
323335
stmt.set_sql(operation)
336+
if query_tags:
337+
# Per-statement query tags. The kernel serialises the
338+
# dict (None value -> bare key) into the SEA
339+
# `query_tags` statement conf. ``query_tags`` is
340+
# already ``Dict[str, Optional[str]]`` from the
341+
# connector, which the kernel accepts directly.
342+
stmt.set_query_tags(query_tags)
324343
if parameters:
325344
bind_tspark_params(stmt, parameters)
326345
if async_op:

tests/e2e/test_kernel_backend.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,32 @@ def test_parameterized_query_decimal(conn):
332332
rows = cur.fetchall()
333333
# Server echoes back as decimal.Decimal.
334334
assert str(rows[0][0]) == "-123.45"
335+
336+
337+
def test_query_tags_round_trip(kernel_conn_params):
338+
"""Per-statement query_tags are forwarded to the kernel and accepted
339+
by the server. Smoke-level: a malformed query_tags conf would fail
340+
the execute. (query.history ingestion lag makes a sync tag-readback
341+
assertion infeasible.)"""
342+
with sql.connect(**kernel_conn_params) as c:
343+
with c.cursor() as cur:
344+
cur.execute(
345+
"SELECT 1 AS n",
346+
query_tags={"team": "platform", "production": None},
347+
)
348+
assert cur.fetchall()[0][0] == 1
349+
350+
351+
def test_user_agent_entry_and_http_headers_round_trip(kernel_conn_params):
352+
"""A connection with user_agent_entry (folded into the connector's
353+
User-Agent, then appended to the kernel base UA) and a custom HTTP
354+
header opens and queries cleanly. Replacing the kernel base UA would
355+
break the SEA result disposition (HTTP 400); appending preserves it
356+
— this exercises that end-to-end."""
357+
params = dict(kernel_conn_params)
358+
params["user_agent_entry"] = "kernel-e2e-app"
359+
params["http_headers"] = [("X-Kernel-E2E", "yes")]
360+
with sql.connect(**params) as c:
361+
with c.cursor() as cur:
362+
cur.execute("SELECT 1 AS n")
363+
assert cur.fetchall()[0][0] == 1

tests/unit/test_kernel_client.py

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -332,26 +332,43 @@ def test_execute_command_forwards_parameters_to_bind_param():
332332
assert stmt.execute.called
333333

334334

335-
def test_execute_command_rejects_query_tags():
335+
def test_execute_command_forwards_query_tags():
336+
"""Statement-level query_tags are forwarded to the kernel statement
337+
via set_query_tags (the kernel serialises them into the SEA
338+
query_tags conf). Previously rejected with NotSupportedError; now
339+
wired (kernel PR adding Statement.set_query_tags)."""
336340
c = _make_client()
337341
c._kernel_session = MagicMock()
338342
cursor = MagicMock()
339343
cursor.arraysize = 100
340344
cursor.buffer_size_bytes = 1024
341-
with pytest.raises(NotSupportedError, match="query_tags"):
342-
c.execute_command(
343-
operation="SELECT 1",
344-
session_id=MagicMock(),
345-
max_rows=1,
346-
max_bytes=1,
347-
lz4_compression=False,
348-
cursor=cursor,
349-
use_cloud_fetch=False,
350-
parameters=[],
351-
async_op=False,
352-
enforce_embedded_schema_correctness=False,
353-
query_tags={"team": "x"},
354-
)
345+
346+
stmt = MagicMock()
347+
stmt.set_sql = MagicMock()
348+
stmt.set_query_tags = MagicMock()
349+
stmt.execute.return_value = MagicMock(
350+
statement_id="stmt-id",
351+
arrow_schema=MagicMock(return_value=pa.schema([("x", pa.int64())])),
352+
)
353+
c._kernel_session.statement.return_value = stmt
354+
355+
tags = {"team": "platform", "production": None}
356+
c.execute_command(
357+
operation="SELECT 1",
358+
session_id=MagicMock(),
359+
max_rows=1,
360+
max_bytes=1,
361+
lz4_compression=False,
362+
cursor=cursor,
363+
use_cloud_fetch=False,
364+
parameters=[],
365+
async_op=False,
366+
enforce_embedded_schema_correctness=False,
367+
query_tags=tags,
368+
)
369+
370+
stmt.set_query_tags.assert_called_once_with(tags)
371+
assert stmt.execute.called
355372

356373

357374
def test_get_columns_accepts_none_catalog():
@@ -1015,3 +1032,48 @@ def test_retry_delay_default_has_no_mapping(self):
10151032
# recognised key here — it has no kernel equivalent.
10161033
out = kernel_client._kernel_retry_kwargs({"retry_delay_default": 5.0})
10171034
assert out == {}
1035+
1036+
1037+
class TestKernelHttpHeadersForwarding:
1038+
"""http_headers (the connector's caller headers + composed
1039+
User-Agent + SPOG org-id) are forwarded to the kernel Session as the
1040+
``http_headers`` kwarg. The kernel applies them per request (its own
1041+
Authorization / org-id win; a caller User-Agent is appended to the
1042+
kernel base UA)."""
1043+
1044+
def _open_capturing(self, monkeypatch, http_headers):
1045+
captured = {}
1046+
1047+
def fake_session(**kw):
1048+
captured.update(kw)
1049+
sess = MagicMock()
1050+
sess.session_id = "sess-id"
1051+
return sess
1052+
1053+
monkeypatch.setattr(kernel_client._kernel, "Session", fake_session)
1054+
c = kernel_client.KernelDatabricksClient(
1055+
server_hostname="example.cloud.databricks.com",
1056+
http_path="/sql/1.0/warehouses/abc",
1057+
auth_provider=AccessTokenAuthProvider("dapi-test"),
1058+
ssl_options=None,
1059+
http_headers=http_headers,
1060+
)
1061+
c.open_session(session_configuration=None, catalog=None, schema=None)
1062+
return captured
1063+
1064+
def test_http_headers_forwarded_to_kernel_session(self, monkeypatch):
1065+
headers = [
1066+
("User-Agent", "PyDatabricksSqlConnector/4.0 (myentry)"),
1067+
("X-Custom", "v1"),
1068+
]
1069+
captured = self._open_capturing(monkeypatch, headers)
1070+
assert captured.get("http_headers") == [
1071+
("User-Agent", "PyDatabricksSqlConnector/4.0 (myentry)"),
1072+
("X-Custom", "v1"),
1073+
]
1074+
1075+
def test_no_http_headers_omits_kwarg(self, monkeypatch):
1076+
# Empty/none headers → the kwarg isn't passed at all (kernel
1077+
# keeps its defaults).
1078+
captured = self._open_capturing(monkeypatch, [])
1079+
assert "http_headers" not in captured

0 commit comments

Comments
 (0)