Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion KERNEL_REV
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4d301455f7e70de9cb747e0f1143b8a3df8c5b48
946c26361872cdf1756614a19aa22b5c602f0824
62 changes: 56 additions & 6 deletions connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
if c.cfg.UseKernel {
be, err = newKernelBackend(ctx, c.cfg)
} else {
// The experimental WithKernel* TLS options have no Thrift-path equivalent —
// reject them loudly rather than silently ignore, so a caller who sets a
// trusted-CA bundle / an independent hostname skip and forgets WithUseKernel
// learns the option had no effect instead of connecting with a
// weaker-than-intended (or unconfigured) TLS trust store.
// The experimental WithKernel* options have no Thrift-path equivalent —
// reject them loudly rather than silently ignore, so a caller who sets one
// (a trusted-CA bundle, an independent hostname skip, an mTLS client
// certificate, or a CloudFetch toggle) and forgets WithUseKernel learns the
// option had no effect instead of connecting with a weaker-than-intended
// (or unconfigured) setup.
if c.cfg.KernelExperimental != nil {
return nil, fmt.Errorf("databricks: a WithKernel* option "+
"(WithKernelTrustedCerts / WithKernelSkipHostnameVerify) %w; "+
"(WithKernelTrustedCerts / WithKernelSkipHostnameVerify / "+
"WithKernelClientCertificate / WithKernelCloudFetch) %w; "+
"add WithUseKernel(true) or remove it", dbsqlerr.ErrRequiresKernelBackend)
}
be, err = thrift.New(ctx, c.cfg, c.client)
Expand Down Expand Up @@ -603,3 +605,51 @@ func WithKernelSkipHostnameVerify() ConnOption {
kernelExperimental(c).TLSSkipHostnameVerify = true
}
}

// WithKernelClientCertificate configures a client certificate + private key for
// mutual TLS (mTLS) on the kernel backend. cert is a PEM leaf certificate,
// optionally followed by its intermediate chain; key is the matching PEM private
// key (use PKCS#8 for portability). Both halves are required together — they map
// to the single paired kernel_session_config_set_tls_client_certificate, so
// cert-without-key is unrepresentable. The private key is never logged.
//
// EXPERIMENTAL, kernel-only: the default (Thrift) backend rejects this at connect.
func WithKernelClientCertificate(cert, key []byte) ConnOption {
return func(c *config.Config) {
// Copy defensively (matching KernelExperimentalConfig.DeepCopy) so a
// caller mutating the buffers between NewConnector and Connect can't change
// the identity out from under us.
k := kernelExperimental(c)
// Record that mTLS was explicitly requested, independent of whether the
// caller passed non-empty bytes. Without this marker an empty cert+key
// pair (e.g. from a failed PEM load) is indistinguishable from the option
// never being called, and validateKernelConfig would accept it while
// applyKernelTLS silently skipped the setter — connecting with no client
// identity. The marker lets validation reject the incomplete request loudly.
k.TLSClientCertConfigured = true
if len(cert) > 0 {
k.TLSClientCertPEM = append([]byte(nil), cert...)
} else {
k.TLSClientCertPEM = cert
}
if len(key) > 0 {
k.TLSClientKeyPEM = append([]byte(nil), key...)
} else {
k.TLSClientKeyPEM = key
}
}
}

// WithKernelCloudFetch toggles CloudFetch (cloud-storage-backed download of large
// result sets) on the kernel backend. Passing false forces the inline-Arrow path
// for all result sizes. Unlike the backend-neutral WithCloudFetch (a plain bool
// whose unset state is indistinguishable from false), this is tri-state: not
// calling it leaves the kernel default (CloudFetch on), and it maps to
// kernel_session_config_set_cloudfetch_enabled only when set.
//
// EXPERIMENTAL, kernel-only: the default (Thrift) backend rejects this at connect.
func WithKernelCloudFetch(enabled bool) ConnOption {
return func(c *config.Config) {
kernelExperimental(c).CloudFetchEnabled = &enabled
}
}
69 changes: 46 additions & 23 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,30 +195,44 @@ level the lines are suppressed with no cost.

dbsql.SetLogLevel("debug") // or DATABRICKS_LOG_LEVEL=debug

The same level is mapped into the kernel's internal (Rust) log subscriber, with two
caveats specific to the Rust lines: they go to stderr directly (not affected by
logger.SetLogOutput), and their verbosity is fixed when the first kernel session in
the process is opened — set the level before that first connect to control them.
The same level is mapped into the kernel's internal (Rust) log subscriber, which is
routed into the driver's logger via a reverse-call callback (kernel_set_log_callback):
kernel-internal tracing events are forwarded into the SAME unified sink as the Go
binding lines — including a custom logger.SetLogOutput — rather than going to stderr.
One caveat specific to the Rust lines: BOTH their verbosity AND their output
destination are captured when the first kernel session in the process is opened (the
forwarding sink snapshots the logger then). A later dbsql.SetLogLevel or
logger.SetLogOutput re-targets the Go binding lines but NOT the already-forwarded
kernel lines, so set the level and any custom output before that first connect to
govern them.

For finer control of the Rust verbosity independent of the driver level, set
DBSQL_KERNEL_DEBUG to any non-empty value: it forces the kernel subscriber on and
defers to RUST_LOG. Filter on the target databricks::sql::kernel (note the colons):
DBSQL_KERNEL_DEBUG to any non-empty value: the callback then defers its level to
RUST_LOG. Filter on the target databricks::sql::kernel (note the colons):

# kernel logs only, at the kernel's own verbosity:
DBSQL_KERNEL_DEBUG=1 RUST_LOG=databricks::sql::kernel=debug ./your_app 2>&1
# kernel logs at the kernel's own verbosity, forwarded into the driver logger:
DBSQL_KERNEL_DEBUG=1 RUST_LOG=databricks::sql::kernel=debug ./your_app
# kernel logs plus its HTTP stack:
DBSQL_KERNEL_DEBUG=1 RUST_LOG=debug ./your_app 2>&1
DBSQL_KERNEL_DEBUG=1 RUST_LOG=debug ./your_app

The kernel exposes one process-global tracing subscriber reachable two mutually
exclusive ways over the C ABI (kernel_init_logging → stderr, kernel_set_log_callback
→ the driver logger); the driver installs the callback. A host that has already
installed its own global tracing subscriber will see the registration report a
benign, logged failure and kernel events simply won't forward.

Supported on the kernel backend: PAT and OAuth (M2M via WithClientCredentials, U2M
via the authType=oauthU2M DSN param); reading scalar, nested, and complex-typed
results (CloudFetch is transparent); bound query parameters (positional and named);
context cancellation during execute; the initial namespace (WithInitialNamespace,
applied post-connect via USE CATALOG / USE SCHEMA); metric-view metadata
(WithEnableMetricViewMetadata); and the TLS, proxy, and session-conf (query tags,
statement timeout, time zone) options. Nothing is silently ignored: WithTimeout, a
retries-disabling WithRetries, token-provider / external / federated
authenticators, and custom M2M OAuth scopes (the kernel applies its own) are
rejected at connect; staging (PUT/GET/REMOVE on a Unity Catalog volume) is
context cancellation during execute and mid-fetch; the initial namespace
(WithInitialNamespace, applied post-connect via USE CATALOG / USE SCHEMA);
metric-view metadata (WithEnableMetricViewMetadata); and the TLS, proxy, and
session-conf (query tags, statement timeout, time zone) options. Nothing is silently
ignored: WithTimeout, a retries-disabling WithRetries, a CloudFetch-disabling
WithCloudFetch(false) (use WithKernelCloudFetch(false) instead), a non-default
WithPort, a non-https protocol, a custom WithTransport, token-provider / external /
federated authenticators, and custom M2M OAuth scopes (the kernel applies its own)
are rejected at connect; staging (PUT/GET/REMOVE on a Unity Catalog volume) is
rejected at execute. WithMaxRows and positive-limit WithRetries are
accepted but inert (the kernel manages fetching and retries below the C ABI).

Expand All @@ -233,15 +247,22 @@ backend exposes a U2M-scopes option, so this is a fixed difference between the t
default sets, not a dropped setting; both authorize against the built-in public
client.

Experimental kernel-only TLS options (rejected by the default backend; the
WithKernel* prefix marks them experimental):
Experimental kernel-only options (rejected by the default backend; the WithKernel*
prefix marks them experimental):
- WithKernelTrustedCerts(pem) adds a PEM CA bundle on top of the system roots (for
a re-signing proxy or on-prem CA). Required because the kernel's TLS stack does
not read SSL_CERT_FILE.
- WithKernelSkipHostnameVerify() skips only the hostname check while keeping chain
validation (finer-grained than WithSkipTLSHostVerify).

Setting either without WithUseKernel fails Connect with an error wrapping the
- WithKernelClientCertificate(cert, key) configures a client certificate + private
key for mutual TLS (mTLS). Both PEM halves are required together; the key is
never logged.
- WithKernelCloudFetch(enabled) toggles CloudFetch. Tri-state: leaving it unset
keeps the kernel default (on); false forces the inline-Arrow path for all result
sizes. Distinct from the backend-neutral WithCloudFetch, whose plain-bool unset
state can't be told apart from false.

Setting any of these without WithUseKernel fails Connect with an error wrapping the
sentinel ErrRequiresKernelBackend, detectable with errors.Is.

Features above the backend seam are inherited unchanged: the database/sql connection
Expand All @@ -253,9 +274,11 @@ and GEOMETRY (WKT). The server query id is surfaced on the success path, so a
QueryIdCallback (see below) fires with the real id and EXECUTE_STATEMENT telemetry
carries it.

On the read path, context cancellation is honored at result-batch boundaries, not
mid-fetch: an in-flight CloudFetch batch runs to completion before the cancel takes
effect.
On the read path, context cancellation is honored both at result-batch boundaries
and mid-fetch: a cancel or deadline firing while a CloudFetch batch is in flight
aborts that fetch (a hung S3 / pre-signed-URL GET does not block the caller past its
deadline), rather than running the batch to completion first. A non-cancellable
context (e.g. context.Background) takes the plain fetch path with no added overhead.

# Programmatically Retrieving Connection and Query Id

Expand Down
23 changes: 23 additions & 0 deletions internal/backend/kernel/abi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kernel

import "fmt"

// compareABI reports whether the C-ABI version compiled into the linked kernel
// library (got) matches the version the driver's header declares (want): it
// returns a descriptive, operator-facing error on mismatch and nil when they
// agree.
//
// It is split out from checkABIVersion (in the cgo-tagged cgo.go) as pure,
// cgo-free logic so the mismatch verdict and its message are unit-testable under
// the default CGO_ENABLED=0 build. The negative path can't otherwise be reached
// from a test: a real build always links a .a and header produced together, and
// abiVersions() reads fixed cgo constants with no injection seam. checkABIVersion
// runs abiVersions() through this once per process (sync.Once-cached), on the
// first connect.
func compareABI(got, want uint32) error {
if got != want {
return fmt.Errorf("databricks: kernel ABI version mismatch: linked library reports %d, "+
"driver header expects %d; rebuild the kernel static lib and header together (make kernel-lib)", got, want)
}
return nil
}
37 changes: 37 additions & 0 deletions internal/backend/kernel/abi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package kernel

import (
"strings"
"testing"
)

// TestCompareABI covers both verdicts of the ABI-version handshake without the
// cgo symbols, so it runs under the default CGO_ENABLED=0 build. The matching
// (happy) path is additionally proven against the real linked library by
// TestABIVersionMatches in the cgo build; the mismatch path — the runtime hazard
// the check exists for (a driver header linked against a differently-built
// prebuilt .a) — is otherwise unreachable from a test, since a real build always
// links a matching .a + header.
func TestCompareABI(t *testing.T) {
// Matching versions must produce no error: checkABIVersion opens the session.
for _, v := range []uint32{0, 1, 42} {
if err := compareABI(v, v); err != nil {
t.Errorf("compareABI(%d, %d) = %v, want nil for matching versions", v, v, err)
}
}

// Mismatched versions must produce an error so checkABIVersion refuses to open
// (rather than silently misread status codes / error-struct fields). The
// message must name both versions and the remediation so an operator hitting a
// stale prebuilt .a knows what to rebuild.
err := compareABI(2, 1)
if err == nil {
t.Fatal("compareABI(2, 1) = nil, want an error for mismatched versions")
}
msg := err.Error()
for _, want := range []string{"ABI version mismatch", "reports 2", "expects 1", "make kernel-lib"} {
if !strings.Contains(msg, want) {
t.Errorf("mismatch error %q does not contain %q", msg, want)
}
}
}
Loading
Loading