fix(kernel): Thrift-parity fixes + GetArrowBatches + U2M scopes (TIMESTAMP, DDL, User-Agent, VOID, Arrow batches, U2M scopes, auth logging)#416
Conversation
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>
838bb0c to
21ad6fa
Compare
…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>
db597fd to
8815db9
Compare
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 { |
There was a problem hiding this comment.
make this change in kernel not here
There was a problem hiding this comment.
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 != "" { |
There was a problem hiding this comment.
please verify from query history
There was a problem hiding this comment.
verified, the behavior is sound and matches how the python driver passes its user agent and what the query history shows
| "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" |
There was a problem hiding this comment.
can you please confirm this NULL to string in other drivers too?
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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>
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>
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>
What
SEA-via-kernel↔Thrift parity fixes surfaced by the Go comparator (two
database/sqlconnections against the same warehouse, differing only inuseKernel), 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 anint64-nanosecond intermediate (value * unit-multiplier). For a microsecond TIMESTAMP — Databricks' wire unit — that overflowsint64outside ~1678–2262 and silently wraps a valid instant to a wrong one:TIMESTAMP '0001-01-01'scanned back as1754-08-30,'9999-12-31'as1816-03-30. Now converts via the unit-specifictime.Unix/UnixMilli/UnixMicroconstructors, 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
-1for "not applicable / unknown" affected-row counts (DDL, SELECT, or a warehouse that doesn't surface the counter — the kernel'snum_modified_rowsOption<i64>None). The Thrift path reports0in those cases, soResult.RowsAffected()diverged: aCREATE/DROP SCHEMAreturned-1on the kernel backend vs0on Thrift. The-1sentinel is now folded to0; 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;WithUserAgentEntrystill customizes it.VOID/NULL columns reported as
NULLinstead ofSTRING. The kernel mapped anarrow.NULLcolumn toDatabaseTypeName="NULL"/nil scan type, but the server stringifies VOID over the Thrift wire — verified live, even a bareSELECT NULLreportsSTRING_TYPE, same as it does for intervals. Sosql.ColumnTypediverged between backends for any null/void column. Now mapsarrow.NULLtoSTRING(unbounded length, string scan type), matching Thrift. Also corrects two pure-Go parity tests that had pinnedNULL_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
GetArrowBatchesnot exposed on the kernel path.kernelRowsimplemented onlydriver.Rows+ theColumnType*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 implementsGetArrowBatchesby wrapping the existing zero-copynext_batchpull (prefetch one batch soHasNextis exact; transfer each record's ownership to the caller so the row scanner andClosecan't double-release).GetArrowIPCStreamsis rejected withErrNotSupportedByKernel— the kernel C ABI exports Arrow via the C Data Interface, not IPC bytes; callers should useGetArrowBatches(or the Thrift backend for IPC streams). A compile-timerows.Rowsassertion guards the gap so it can't silently regress.U2M OAuth scope parity
all-apis + offline_access. The Thrift path requestsoffline_access + sql(AWS/GCP) oroffline_access + <tenant>/user_impersonation(Azure) viaoauth.GetScopes. A workspace whose built-indatabricks-sql-connectorclient isn't grantedall-apisrejects the kernel's request withaccess_denied - Scopes 'all-apis' are not assigned.resolveKernelAuthnow populatesAuth.Scopesfrom the sameoauth.GetScopesthe Thrift path uses, so both backends authorize identically. ThesetAuth → set_auth_u2mwiring already forwardedScopes; only the population was missing. No kernel/C-ABI change.Auth logging
zerolog/loglogger (default level Debug), so lines like"databricks OAuth token fetched successfully"printed as raw JSON regardless ofWithLogLevel— on both backends. Routed every one of them through the driver'sloggerpackage so they honor the configured level and use the driver's format;Error/Warnlines stay visible at the default Warn,Debug/Infogate off. Auth logic is untouched. This closes the last global-zerolog call sites in the driver, soDATABRICKS_LOG_LEVEL=debugnow uniformly controls auth logging too.Testing
TestScanCellTimestampOutOfNanoRange— pins both TIMESTAMP repro endpoints (0001-01-01,9999-12-31) plus theint64-ns boundary years; existing timestamp unit/location tests still pass.TestNormalizeAffectedRows— table test for the DDL sentinel-normalization rule, in a pure (untagged)normalizeAffectedRowsso it runs under the defaultCGO_ENABLED=0build.TestColumnTypeInfoFor/TestColumnTypeInfoMatchesThriftMapping/TestColumnTypeInfoScanTypeCoversScanner— the VOID→STRING mapping and its two cross-checks against the Thrift functions; all green.GetArrowBatchesverified live against staging on the kernel backend via aConn.Raw+rows.Rowstype-assertion harness: 100,000 rows over 32 zero-copyarrow.Recordbatches, row-count and schema checks pass; the compile-timerows.Rowsassertion pins the interface.GetArrowIPCStreamsreturns theErrNotSupportedByKernelsentinel.TestValidateKernelConfigandTestResolveKernelAuthRealAuthenticators(fake + real U2M authenticator) now assert the resolvedAuth.Scopesequaloauth.GetScopes(host);TestSetAuthByMode's "U2M full" case still pins theset_auth_u2mscope/port marshalling../auth/...package tests green after the logger swaps; no import cycle (theloggerpackage depends only on zerolog + go-isatty).CGO_ENABLED=0) and tagged cgo (-tags databricks_kernel, linking the pinned kernel.a) — build + unit tests green on each.databricks-driver-testGo comparator, Thrift vs SEA): the pre-fix run was at 13 diffs vs drivermain's 41; the latest run against this branch head is at 9. The 5dt_interval_columnmetadata diffs (databaseTypeName=""/scanType=*interface{}on the kernel path vsSTRING/stringon Thrift, across 5 connection configs —ColumnTypeInfoForand the value scanner lackedarrow.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:NaN), non-deterministic EXPLAIN plan node IDs (same plan), by-design volume-staging rejection (ErrNotSupportedByKernel), raw-fetch_arrowschema/value shape (×2 — see below), and a CREATE PROCEDUREsqlStatedetail on a permission-gated statement (both sides fail; only the error detail differs).STATEMENT_DDLdiff (deterministic, under investigation):DESCRIBE TABLEon the SEA/kernel side returns two extra trailing rows Thrift does not —# Clustering Informationand# col_namesection 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 carriesresult_set_filters(skip_rows) while the Go comparator config currently hasresult_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-sideskip_rowsfilter (indatabricks-driver-test, not this repo), pending a direct Thrift-vs-kernel DESCRIBE reproduction.fetch_arrowpath (part of the residual, and shared root with intervals): the kernel returns nativedecimal128/ native intervals while Thrift's server pre-stringifies toutf8; the Go driver also formats decimals per-cell viadecimalfmt.ExactStringon 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'sintervals_as_string. Tracked as a separate follow-up (kernelResultConfigdecimal_as_string, needs a kernel change +KERNEL_REVbump) — PECOBLR-3838.Stacked on
mani/sea-kernel-new-items(#412).Co-authored-by: Isaac