Skip to content

fix(kernel): Thrift-parity fixes + GetArrowBatches + U2M scopes (TIMESTAMP, DDL, User-Agent, VOID, Arrow batches, U2M scopes, auth logging)#416

Open
mani-mathur-arch wants to merge 12 commits into
mainfrom
mani/sea-kernel-comparator-fixes
Open

fix(kernel): Thrift-parity fixes + GetArrowBatches + U2M scopes (TIMESTAMP, DDL, User-Agent, VOID, Arrow batches, U2M scopes, auth logging)#416
mani-mathur-arch wants to merge 12 commits into
mainfrom
mani/sea-kernel-comparator-fixes

Conversation

@mani-mathur-arch

@mani-mathur-arch mani-mathur-arch commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

What

SEA-via-kernel↔Thrift parity fixes surfaced by the Go comparator (two database/sql connections against the same warehouse, differing only in useKernel), plus the public Arrow batch API on the kernel path and an auth-logging cleanup. The parity fixes are kernel-path-only divergences from the default Thrift path and are entirely Go-side (no kernel/C-ABI change).

Kernel ↔ Thrift result/parity fixes

  • TIMESTAMP out-of-nanosecond-range wrapping. The kernel Rows path converted arrow timestamps with arrow's ToTime, which forms an int64-nanosecond intermediate (value * unit-multiplier). For a microsecond TIMESTAMP — Databricks' wire unit — that overflows int64 outside ~1678–2262 and silently wraps a valid instant to a wrong one: TIMESTAMP '0001-01-01' scanned back as 1754-08-30, '9999-12-31' as 1816-03-30. Now converts via the unit-specific time.Unix/UnixMilli/UnixMicro constructors, which split into (seconds, nanoseconds) and represent the full 0001–9999 range. The default Thrift path is unaffected — it receives TIMESTAMP as a preformatted server string and never runs this multiply.

  • DDL affected-rows reported as -1 instead of 0. The C ABI returns -1 for "not applicable / unknown" affected-row counts (DDL, SELECT, or a warehouse that doesn't surface the counter — the kernel's num_modified_rows Option<i64> None). The Thrift path reports 0 in those cases, so Result.RowsAffected() diverged: a CREATE/DROP SCHEMA returned -1 on the kernel backend vs 0 on Thrift. The -1 sentinel is now folded to 0; a real DML count (>= 0) passes through unchanged.

  • User-Agent not forwarded on the kernel path. The kernel backend never forwarded the driver's composed User-Agent, so the kernel's built-in UA leaked through and query history misattributed SEA-path queries. Now forwards the same UA the Thrift path sends via set_custom_header, so both backends are attributed alike. Default-on; WithUserAgentEntry still customizes it.

  • VOID/NULL columns reported as NULL instead of STRING. The kernel mapped an arrow.NULL column to DatabaseTypeName="NULL"/nil scan type, but the server stringifies VOID over the Thrift wire — verified live, even a bare SELECT NULL reports STRING_TYPE, same as it does for intervals. So sql.ColumnType diverged between backends for any null/void column. Now maps arrow.NULL to STRING (unbounded length, string scan type), matching Thrift. Also corrects two pure-Go parity tests that had pinned NULL_TYPE — that expectation was derived from the enum name, not captured live, and the live server disagrees.

Public Arrow batch API on the kernel path

  • GetArrowBatches not exposed on the kernel path. kernelRows implemented only driver.Rows + the ColumnType* interfaces, so the public batch API — conn.Raw(...)rows.(rows.Rows).GetArrowBatches(ctx) — failed its type assertion on the kernel backend while succeeding on Thrift. It now implements GetArrowBatches by wrapping the existing zero-copy next_batch pull (prefetch one batch so HasNext is exact; transfer each record's ownership to the caller so the row scanner and Close can't double-release). GetArrowIPCStreams is rejected with ErrNotSupportedByKernel — the kernel C ABI exports Arrow via the C Data Interface, not IPC bytes; callers should use GetArrowBatches (or the Thrift backend for IPC streams). A compile-time rows.Rows assertion guards the gap so it can't silently regress.

U2M OAuth scope parity

  • U2M forwarded no scopes, so the kernel used its own default set. The kernel U2M path forwarded only the client id, so the kernel applied all-apis + offline_access. The Thrift path requests offline_access + sql (AWS/GCP) or offline_access + <tenant>/user_impersonation (Azure) via oauth.GetScopes. A workspace whose built-in databricks-sql-connector client isn't granted all-apis rejects the kernel's request with access_denied - Scopes 'all-apis' are not assigned. resolveKernelAuth now populates Auth.Scopes from the same oauth.GetScopes the Thrift path uses, so both backends authorize identically. The setAuth → set_auth_u2m wiring already forwarded Scopes; only the population was missing. No kernel/C-ABI change.

Auth logging

  • Auth logs bypassed the driver's configured log level. The M2M, U2M, OAuth-config, and token-provider paths logged through the global zerolog/log logger (default level Debug), so lines like "databricks OAuth token fetched successfully" printed as raw JSON regardless of WithLogLevel — on both backends. Routed every one of them through the driver's logger package so they honor the configured level and use the driver's format; Error/Warn lines stay visible at the default Warn, Debug/Info gate off. Auth logic is untouched. This closes the last global-zerolog call sites in the driver, so DATABRICKS_LOG_LEVEL=debug now uniformly controls auth logging too.

Testing

  • TestScanCellTimestampOutOfNanoRange — pins both TIMESTAMP repro endpoints (0001-01-01, 9999-12-31) plus the int64-ns boundary years; existing timestamp unit/location tests still pass.
  • TestNormalizeAffectedRows — table test for the DDL sentinel-normalization rule, in a pure (untagged) normalizeAffectedRows so it runs under the default CGO_ENABLED=0 build.
  • TestColumnTypeInfoFor / TestColumnTypeInfoMatchesThriftMapping / TestColumnTypeInfoScanTypeCoversScanner — the VOID→STRING mapping and its two cross-checks against the Thrift functions; all green.
  • GetArrowBatches verified live against staging on the kernel backend via a Conn.Raw + rows.Rows type-assertion harness: 100,000 rows over 32 zero-copy arrow.Record batches, row-count and schema checks pass; the compile-time rows.Rows assertion pins the interface. GetArrowIPCStreams returns the ErrNotSupportedByKernel sentinel.
  • U2M scope parityTestValidateKernelConfig and TestResolveKernelAuthRealAuthenticators (fake + real U2M authenticator) now assert the resolved Auth.Scopes equal oauth.GetScopes(host); TestSetAuthByMode's "U2M full" case still pins the set_auth_u2m scope/port marshalling.
  • Auth logging./auth/... package tests green after the logger swaps; no import cycle (the logger package depends only on zerolog + go-isatty).
  • Verified on both build paths: default pure-Go (CGO_ENABLED=0) and tagged cgo (-tags databricks_kernel, linking the pinned kernel .a) — build + unit tests green on each.
  • Comparator run (databricks-driver-test Go comparator, Thrift vs SEA): the pre-fix run was at 13 diffs vs driver main's 41; the latest run against this branch head is at 9. The 5 dt_interval_column metadata diffs (databaseTypeName=""/scanType=*interface{} on the kernel path vs STRING/string on Thrift, across 5 connection configs — ColumnTypeInfoFor and the value scanner lacked arrow.INTERVAL_DAY_TIME) are fixed in this PR (map INTERVAL_DAY_TIME → STRING for both metadata and value scanning) and are confirmed gone in the latest run. Of the remaining 9:
    • 8 are not driver bugs: NaN-vs-NaN comparator equality artifact (×3, both sides NaN), non-deterministic EXPLAIN plan node IDs (same plan), by-design volume-staging rejection (ErrNotSupportedByKernel), raw-fetch_arrow schema/value shape (×2 — see below), and a CREATE PROCEDURE sqlState detail on a permission-gated statement (both sides fail; only the error detail differs).
    • 1 new STATEMENT_DDL diff (deterministic, under investigation): DESCRIBE TABLE on the SEA/kernel side returns two extra trailing rows Thrift does not — # Clustering Information and # col_name section markers. The driver does no DESCRIBE post-processing (it scans server rows verbatim), so this is server/backend output, not a driver-rendering bug; the Python comparator does not surface it because its config carries result_set_filters (skip_rows) while the Go comparator config currently has result_set_filters: []. Caveat/open item: it reproduces across runs (not flaky) and was reportedly not present before recent changes; no mechanism has been found by which this PR's changes (arrow interval/decimal metadata, RowsAffected, auth logging, GetArrowBatches) could alter DESCRIBE row count, so causation is unresolved. Likely resolution is a comparator-side skip_rows filter (in databricks-driver-test, not this repo), pending a direct Thrift-vs-kernel DESCRIBE reproduction.
    • Decimal on the raw-fetch_arrow path (part of the residual, and shared root with intervals): the kernel returns native decimal128 / native intervals while Thrift's server pre-stringifies to utf8; the Go driver also formats decimals per-cell via decimalfmt.ExactString on the row-scan path. On the Arrow-batch path native types are arguably the desired behavior, so this is a tradeoff, not a bug. Cross-driver note: Python avoids the equivalent diff by setting the kernel's intervals_as_string. Tracked as a separate follow-up (kernel ResultConfig decimal_as_string, needs a kernel change + KERNEL_REV bump) — PECOBLR-3838.

Stacked on mani/sea-kernel-new-items (#412).

Co-authored-by: Isaac

Base automatically changed from mani/sea-kernel-new-items to main July 21, 2026 04:09
The kernel Rows path converted arrow timestamps with arrow's ToTime, which
forms an int64-nanosecond intermediate (value * unit-multiplier). For a
microsecond TIMESTAMP — Databricks' wire unit — that overflows int64 outside
~1678-2262 and silently wraps a valid instant to a wrong one (TIMESTAMP
'0001-01-01' scanned back as 1754-08-30, '9999-12-31' as 1816-03-30).

Convert via the unit-specific time.Unix/UnixMilli/UnixMicro constructors, which
split into (seconds, nanoseconds) internally and represent the full 0001-9999
range Databricks TIMESTAMP allows. The default Thrift path is unaffected — it
receives TIMESTAMP as a preformatted server string and never runs this
multiply — so this closes a kernel-only divergence from Thrift.

TestScanCellTimestampOutOfNanoRange pins both repro endpoints plus the
int64-ns boundary years; the existing unit and location tests still pass.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
The C ABI returns -1 for "not applicable / unknown" affected-row counts — DDL,
SELECT, or a warehouse that doesn't surface the counter (the kernel's
num_modified_rows Option<i64> None). The Thrift path reports 0 in those cases,
so Result.RowsAffected() diverged: a CREATE/DROP SCHEMA returned -1 on the
kernel backend and 0 on Thrift.

Fold the -1 sentinel to 0 so RowsAffected() is identical across backends; a
real DML count (>= 0) passes through unchanged. The rule lives in a pure,
untagged normalizeAffectedRows so it is unit-testable in the default
CGO_ENABLED=0 build alongside the other kernel decision helpers.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
@mani-mathur-arch
mani-mathur-arch force-pushed the mani/sea-kernel-comparator-fixes branch from 838bb0c to 21ad6fa Compare July 21, 2026 09:16
…iver

The kernel backend never forwarded the driver's User-Agent, so the kernel's
built-in UA leaked through and query history misattributed SEA-path queries.
Forward the same composed UA the Thrift path sends via set_custom_header, so
both backends are attributed alike. Default-on; WithUserAgentEntry still
customizes it.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
The server stringifies VOID over the Thrift wire (verified live: even bare
SELECT NULL reports STRING_TYPE), same as intervals, but hands the kernel a
native arrow.NULL schema. Map arrow.NULL to STRING so both backends report
identical column metadata; correct the two parity tests that wrongly pinned
NULL_TYPE (derived from the enum name, never captured live).

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
@mani-mathur-arch
mani-mathur-arch force-pushed the mani/sea-kernel-comparator-fixes branch from db597fd to 8815db9 Compare July 22, 2026 18:51
@mani-mathur-arch mani-mathur-arch changed the title fix(kernel): TIMESTAMP range + DDL affected-rows parity with Thrift fix(kernel): Thrift parity fixes (TIMESTAMP range, DDL rows, User-Agent, VOID type) Jul 22, 2026
Address review of the parity fixes: assert kc.UserAgent == BuildUserAgent
(non-empty) in TestBuildKernelConfig so deleting the forwarding or an empty UA
fails a pure-Go run; add a CAST(NULL AS VOID) column to the live column-type
parity query so the arrow.NULL→STRING mapping is guarded against the real
server, not only the pure-Go tests. Trim the duplicated -1→0 rationale at the
affected-rows call site to a one-liner.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
// None). The Thrift path reports 0 in exactly those cases (TGetOperationStatusResp
// defaults NumModifiedRows to 0), so the -1 sentinel is folded to 0 for parity.
// A real DML count (>= 0) passes through unchanged.
func normalizeAffectedRows(n int64) int64 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this change in kernel not here

@mani-mathur-arch mani-mathur-arch Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changing this in the kernel will break the other drivers. other drivers have different specs for each "not applicable" row counts (python has -1, node has none). Go thrift has 0 for the database/sql spec.

}

// User-Agent so query history attributes the kernel path to this driver.
if k.cfg.UserAgent != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please verify from query history

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verified, the behavior is sound and matches how the python driver passes its user agent and what the query history shows

Comment thread kernel_parity_test.go
"INTERVAL '1' MONTH a_iv_ym, st_point(1,2) a_geom"
// VOID: the server declares it STRING_TYPE over Thrift; asserts the kernel
// arrow.NULL→STRING mapping matches live, not just in the pure-Go tests.
"INTERVAL '1' MONTH a_iv_ym, st_point(1,2) a_geom, CAST(NULL AS VOID) a_void"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please confirm this NULL to string in other drivers too?

@mani-mathur-arch mani-mathur-arch Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other drivers (node and python) also have this gap, the kernel returns nulls as void but thrift returns as string. i think this is an intentional design decision. verified by the python comparator, node one is currently broken due to dependency differences. I can remove it if that should be taken up as a separate fix for all the drivers or at the kernel level

@msrathore-db msrathore-db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please confirm the similar issues in other drivers over kernel like node and python so we can decide the location of these fixes

@msrathore-db
msrathore-db self-requested a review July 23, 2026 21:50

@msrathore-db msrathore-db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify other drivers

kernelRows implemented only driver.Rows + the ColumnType* interfaces, so a
Conn.Raw + rows.(dbsqlrows.Rows) assertion failed on the kernel backend (it
succeeds on Thrift). Implement GetArrowBatches by wrapping the existing zero-copy
next_batch pull, and reject GetArrowIPCStreams (the kernel C ABI exports Arrow C
Data, not IPC bytes). A compile-time dbrows.Rows assertion guards the gap.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
m2m.Authenticate logged via the global zerolog logger, whose default level is
Debug, so "OAuth token fetched successfully" printed regardless of the driver's
configured log level (default Warn) on both backends. Route it through the
driver's logger package so it honors WithLogLevel like every other line.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
@mani-mathur-arch mani-mathur-arch changed the title fix(kernel): Thrift parity fixes (TIMESTAMP range, DDL rows, User-Agent, VOID type) fix(kernel): Thrift parity fixes + GetArrowBatches (TIMESTAMP, DDL, User-Agent, VOID, Arrow batches, M2M logging) Jul 24, 2026
The U2M, OAuth-config, and token-provider paths still logged via the global
zerolog logger (default level Debug), so their lines printed as raw JSON
regardless of the driver's configured level — the same leak the m2m fix
addressed. Route all of them through the driver's logger package so every auth
log honors WithLogLevel and uses the driver's format; auth logic is untouched.
This closes the last global-zerolog call sites in the driver.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
The kernel U2M path forwarded only the client id, so the kernel applied its own
default scope set (all-apis + offline_access). A workspace whose built-in
databricks-sql-connector client isn't granted all-apis rejects that with
access_denied. resolveKernelAuth now populates Auth.Scopes from oauth.GetScopes
— the same function the Thrift path uses (offline_access + sql on AWS/GCP,
offline_access + <tenant>/user_impersonation on Azure) — so both backends
authorize identically. The setAuth -> set_auth_u2m wiring already forwarded
Scopes; only the population was missing. No kernel/C-ABI change.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
@mani-mathur-arch mani-mathur-arch changed the title fix(kernel): Thrift parity fixes + GetArrowBatches (TIMESTAMP, DDL, User-Agent, VOID, Arrow batches, M2M logging) fix(kernel): Thrift-parity fixes + GetArrowBatches + U2M scopes (TIMESTAMP, DDL, User-Agent, VOID, Arrow batches, U2M scopes, auth logging) Jul 24, 2026
DAY-TIME intervals can arrive from the kernel as arrow.INTERVAL_DAY_TIME
(*array.DayTimeInterval), not just arrow.DURATION. ColumnTypeInfoFor lacked the
case, so dt_interval columns reported ""/*interface{} vs Thrift's STRING/string
(comparator diff across 5 configs); the value scanner also lacked it, so a real
DT-interval row would have errored. Add both — metadata → STRING and the scanner
folding {Days,Milliseconds} to ms through the same renderer — mirroring the
existing DURATION/INTERVAL_MONTHS handling. Kernel-path only; no other driver
and no kernel/C-ABI change.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
Detach kernelRows.ctx from the caller's QueryContext (context2.WithoutCancel +
Close-driven cancel), matching the Thrift path: a deadline that only gated
statement submission no longer truncates a large CloudFetch stream. Seed
iterationErr on batch-path fetch errors so a failed stream is recorded as a
failed statement in telemetry, not a success. Add kernel-tagged coverage for the
previously-untested batch API: white-box unit tests (IPC rejection, schema,
drained-stream, sticky fetch-error → iterationErr) and a live GetArrowBatches
draining e2e with an exact row count.

Add a literal-assertion TestGetScopes (AWS/GCP/Azure) so a scope regression — the
cause of the kernel U2M access_denied incident — is caught rather than asserted
tautologically. Fix stale comments: the U2M doc now reflects that scopes are
forwarded, m2m references SetLogLevel (not the nonexistent WithLogLevel), and the
m2m success log moved after the error check.

Co-authored-by: Isaac
Signed-off-by: Mani Kaustubh Mathur <mani.mathur@databricks.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants