@fabricorg/databricks-testkit 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,8 +9,8 @@ two profiles:
9
9
  - **local** — DuckDB in-process (`@duckdb/node-api`, optional peer). No
10
10
  workspace, no credentials, sub-second. The default.
11
11
  - **live** — a real Databricks SQL warehouse via the SQL Statement Execution
12
- API, built on `@fabric-harness/databricks`. Always gated by
13
- `DBX_TEST_LIVE=1`.
12
+ API through the testkit's narrow, injectable workspace transport. Always
13
+ gated by `DBX_TEST_LIVE=1`.
14
14
 
15
15
  Positional `?` params work in both profiles — the live context transparently
16
16
  rewrites them to the named markers the Statement Execution API requires.
@@ -1,7 +1,142 @@
1
1
  import { parseFixtureColumns, resolveFixtureRows } from './chunk-F3KU6VZS.js';
2
2
  import { readFile } from 'fs/promises';
3
- import { DatabricksRestClient, waitForDatabricksStatement } from '@fabric-harness/databricks';
4
3
 
4
+ // src/workspace-client.ts
5
+ var WorkspaceRestClient = class {
6
+ host;
7
+ token;
8
+ fetchImpl;
9
+ maxRetries;
10
+ retryBaseDelayMs;
11
+ signal;
12
+ constructor(options) {
13
+ this.host = workspaceOrigin(options.host);
14
+ this.token = options.token;
15
+ this.fetchImpl = options.fetchImpl ?? fetch;
16
+ this.maxRetries = options.maxRetries ?? 3;
17
+ this.retryBaseDelayMs = options.retryBaseDelayMs ?? 500;
18
+ this.signal = options.signal;
19
+ }
20
+ get(path, query) {
21
+ return this.request("GET", path, void 0, query);
22
+ }
23
+ post(path, body, query, options) {
24
+ return this.request("POST", path, body, query, options);
25
+ }
26
+ async request(method, path, body, query = {}, options = {}) {
27
+ const url = new URL(`${this.host}${path.startsWith("/") ? path : `/${path}`}`);
28
+ for (const [key, value] of Object.entries(query)) {
29
+ if (value !== void 0) url.searchParams.set(key, String(value));
30
+ }
31
+ const bodyHasIdempotencyToken = typeof body === "object" && body !== null && typeof body.idempotency_token === "string";
32
+ const canRetry = options.retry === "always" || options.retry !== "never" && (options.retry === "safe" || method === "GET" || Boolean(options.idempotencyKey) || bodyHasIdempotencyToken);
33
+ const maxRetries = canRetry ? options.maxRetries ?? this.maxRetries : 0;
34
+ for (let attempt = 0; ; attempt += 1) {
35
+ const token = typeof this.token === "function" ? await this.token() : this.token;
36
+ let response;
37
+ try {
38
+ response = await this.fetchImpl(url, {
39
+ method,
40
+ headers: {
41
+ authorization: `Bearer ${token}`,
42
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
43
+ ...options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}
44
+ },
45
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
46
+ ...this.signal ? { signal: this.signal } : {}
47
+ });
48
+ } catch (error) {
49
+ if (this.signal?.aborted || attempt >= maxRetries) throw error;
50
+ await delay(this.retryBaseDelayMs * 2 ** attempt, this.signal);
51
+ continue;
52
+ }
53
+ if (response.ok) {
54
+ if (response.status === 204) return void 0;
55
+ return await response.json();
56
+ }
57
+ if (isRetryableStatus(response.status) && attempt < maxRetries) {
58
+ await delay(retryDelayMs(response, attempt, this.retryBaseDelayMs), this.signal);
59
+ continue;
60
+ }
61
+ const message = (await response.text()).split(token).join("[REDACTED]");
62
+ throw new Error(
63
+ `databricks: ${response.status} ${response.statusText}: ${message.slice(0, 2e3)}`
64
+ );
65
+ }
66
+ }
67
+ };
68
+ async function waitForDatabricksRun(client, runId, options = {}) {
69
+ const deadline = Date.now() + (options.timeoutMs ?? 6e5);
70
+ const interval = options.pollIntervalMs ?? 5e3;
71
+ let current = await client.get("/api/2.2/jobs/runs/get", {
72
+ run_id: runId
73
+ });
74
+ while (!isTerminalRun(current) && Date.now() < deadline) {
75
+ await delay(interval, options.signal);
76
+ current = await client.get("/api/2.2/jobs/runs/get", {
77
+ run_id: runId
78
+ });
79
+ }
80
+ return current;
81
+ }
82
+ async function waitForDatabricksStatement(client, statementId, options = {}) {
83
+ const deadline = Date.now() + (options.timeoutMs ?? 3e5);
84
+ const interval = options.pollIntervalMs ?? 2e3;
85
+ const path = `/api/2.0/sql/statements/${encodeURIComponent(statementId)}`;
86
+ let current = await client.get(path);
87
+ while (!isTerminalStatement(current) && Date.now() < deadline) {
88
+ await delay(interval, options.signal);
89
+ current = await client.get(path);
90
+ }
91
+ return current;
92
+ }
93
+ function workspaceOrigin(host) {
94
+ const value = host.trim();
95
+ if (!value) throw new Error("databricks: workspace host cannot be empty");
96
+ const url = new URL(value.includes("://") ? value : `https://${value}`);
97
+ if (url.username || url.password) {
98
+ throw new Error("databricks: workspace host must not contain credentials");
99
+ }
100
+ return url.origin;
101
+ }
102
+ function isRetryableStatus(status) {
103
+ return status === 429 || status === 499 || status >= 500 && status <= 599;
104
+ }
105
+ function retryDelayMs(response, attempt, base) {
106
+ const retryAfterHeader = response.headers.get("retry-after");
107
+ const retryAfter = retryAfterHeader === null ? Number.NaN : Number(retryAfterHeader);
108
+ return Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter * 1e3 : base * 2 ** attempt;
109
+ }
110
+ function isTerminalRun(run) {
111
+ const state = run.state;
112
+ return ["TERMINATED", "SKIPPED", "INTERNAL_ERROR"].includes(state?.life_cycle_state ?? "");
113
+ }
114
+ function isTerminalStatement(statement) {
115
+ const status = statement.status;
116
+ return ["SUCCEEDED", "FAILED", "CANCELED", "CLOSED"].includes(status?.state ?? "");
117
+ }
118
+ function delay(ms, signal) {
119
+ if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
120
+ if (signal.aborted) {
121
+ return Promise.reject(
122
+ signal.reason ?? new DOMException("The operation was aborted", "AbortError")
123
+ );
124
+ }
125
+ return new Promise((resolve, reject) => {
126
+ const timer = setTimeout(done, ms);
127
+ signal.addEventListener("abort", aborted, { once: true });
128
+ function done() {
129
+ signal?.removeEventListener("abort", aborted);
130
+ resolve();
131
+ }
132
+ function aborted() {
133
+ clearTimeout(timer);
134
+ reject(signal?.reason ?? new DOMException("The operation was aborted", "AbortError"));
135
+ }
136
+ });
137
+ }
138
+
139
+ // src/workspace-context.ts
5
140
  async function createWorkspaceContext(options = {}) {
6
141
  const env = options.env ?? process.env;
7
142
  const warehouseId = resolveWarehouseId(env);
@@ -28,11 +163,10 @@ async function createWorkspaceContext(options = {}) {
28
163
  const state = submitted.status?.state;
29
164
  let terminal = submitted;
30
165
  if (state !== "SUCCEEDED" && state !== "FAILED" && state !== "CANCELED" && state !== "CLOSED") {
31
- terminal = await waitForDatabricksStatement(
32
- client,
33
- submitted.statement_id ?? "",
34
- { pollIntervalMs: options.pollIntervalMs, timeoutMs: options.timeoutMs }
35
- );
166
+ terminal = await waitForDatabricksStatement(client, submitted.statement_id ?? "", {
167
+ pollIntervalMs: options.pollIntervalMs,
168
+ timeoutMs: options.timeoutMs
169
+ });
36
170
  }
37
171
  if (terminal.status?.state !== "SUCCEEDED") {
38
172
  throw new Error(
@@ -149,7 +283,7 @@ function resolveWarehouseId(env) {
149
283
  function createClientFromEnv(env, fetchImpl) {
150
284
  const host = env.DATABRICKS_HOST;
151
285
  if (!host) throw new Error("Live profile requires DATABRICKS_HOST");
152
- return new DatabricksRestClient({
286
+ return new WorkspaceRestClient({
153
287
  host,
154
288
  token: resolveTokenProvider(host, env, fetchImpl),
155
289
  ...fetchImpl ? { fetchImpl } : {}
@@ -237,6 +371,6 @@ async function resolveFederatedSubjectToken(authType, env) {
237
371
  return token;
238
372
  }
239
373
 
240
- export { createClientFromEnv, createWorkspaceContext, parseStatementRows, resolveTokenProvider, toNamedParameters };
241
- //# sourceMappingURL=chunk-SCVVMUAF.js.map
242
- //# sourceMappingURL=chunk-SCVVMUAF.js.map
374
+ export { createClientFromEnv, createWorkspaceContext, parseStatementRows, resolveTokenProvider, toNamedParameters, waitForDatabricksRun, waitForDatabricksStatement };
375
+ //# sourceMappingURL=chunk-MV752EAZ.js.map
376
+ //# sourceMappingURL=chunk-MV752EAZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workspace-client.ts","../src/workspace-context.ts"],"names":["token"],"mappings":"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","file":"chunk-MV752EAZ.js","sourcesContent":["/** Minimal request boundary used by the live Databricks certification suite. */\nexport interface WorkspaceClientLike {\n get<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<T>;\n post<T>(\n path: string,\n body?: unknown,\n query?: Record<string, string | number | boolean | undefined>,\n options?: WorkspaceRequestOptions,\n ): Promise<T>;\n}\n\nexport interface WorkspaceClientOptions {\n host: string;\n token: string | (() => string | Promise<string>);\n fetchImpl?: typeof fetch;\n maxRetries?: number;\n retryBaseDelayMs?: number;\n signal?: AbortSignal;\n}\n\nexport interface WorkspaceRequestOptions {\n retry?: 'safe' | 'always' | 'never';\n idempotencyKey?: string;\n maxRetries?: number;\n}\n\nexport interface WorkspaceWaitOptions {\n pollIntervalMs?: number;\n timeoutMs?: number;\n signal?: AbortSignal;\n}\n\n/**\n * Testkit-owned REST boundary for workspace certification endpoints.\n *\n * Harness 4 removed its generic public REST client in favor of generated,\n * capability-specific Databricks SDK clients. The testkit intentionally keeps\n * this narrow injectable transport because it certifies APIs across several\n * services and because its mock client implements the same two-method shape.\n */\nexport class WorkspaceRestClient implements WorkspaceClientLike {\n private readonly host: string;\n private readonly token: WorkspaceClientOptions['token'];\n private readonly fetchImpl: typeof fetch;\n private readonly maxRetries: number;\n private readonly retryBaseDelayMs: number;\n private readonly signal: AbortSignal | undefined;\n\n constructor(options: WorkspaceClientOptions) {\n this.host = workspaceOrigin(options.host);\n this.token = options.token;\n this.fetchImpl = options.fetchImpl ?? fetch;\n this.maxRetries = options.maxRetries ?? 3;\n this.retryBaseDelayMs = options.retryBaseDelayMs ?? 500;\n this.signal = options.signal;\n }\n\n get<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<T> {\n return this.request<T>('GET', path, undefined, query);\n }\n\n post<T>(\n path: string,\n body?: unknown,\n query?: Record<string, string | number | boolean | undefined>,\n options?: WorkspaceRequestOptions,\n ): Promise<T> {\n return this.request<T>('POST', path, body, query, options);\n }\n\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n query: Record<string, string | number | boolean | undefined> = {},\n options: WorkspaceRequestOptions = {},\n ): Promise<T> {\n const url = new URL(`${this.host}${path.startsWith('/') ? path : `/${path}`}`);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) url.searchParams.set(key, String(value));\n }\n const bodyHasIdempotencyToken =\n typeof body === 'object' &&\n body !== null &&\n typeof (body as Record<string, unknown>).idempotency_token === 'string';\n const canRetry =\n options.retry === 'always' ||\n (options.retry !== 'never' &&\n (options.retry === 'safe' ||\n method === 'GET' ||\n Boolean(options.idempotencyKey) ||\n bodyHasIdempotencyToken));\n const maxRetries = canRetry ? (options.maxRetries ?? this.maxRetries) : 0;\n\n for (let attempt = 0; ; attempt += 1) {\n const token = typeof this.token === 'function' ? await this.token() : this.token;\n let response: Response;\n try {\n response = await this.fetchImpl(url, {\n method,\n headers: {\n authorization: `Bearer ${token}`,\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(options.idempotencyKey ? { 'idempotency-key': options.idempotencyKey } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n ...(this.signal ? { signal: this.signal } : {}),\n });\n } catch (error) {\n if (this.signal?.aborted || attempt >= maxRetries) throw error;\n await delay(this.retryBaseDelayMs * 2 ** attempt, this.signal);\n continue;\n }\n\n if (response.ok) {\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n }\n if (isRetryableStatus(response.status) && attempt < maxRetries) {\n await delay(retryDelayMs(response, attempt, this.retryBaseDelayMs), this.signal);\n continue;\n }\n\n const message = (await response.text()).split(token).join('[REDACTED]');\n throw new Error(\n `databricks: ${response.status} ${response.statusText}: ${message.slice(0, 2000)}`,\n );\n }\n }\n}\n\nexport async function waitForDatabricksRun(\n client: WorkspaceClientLike,\n runId: number,\n options: WorkspaceWaitOptions = {},\n): Promise<Record<string, unknown>> {\n const deadline = Date.now() + (options.timeoutMs ?? 600_000);\n const interval = options.pollIntervalMs ?? 5_000;\n let current = await client.get<Record<string, unknown>>('/api/2.2/jobs/runs/get', {\n run_id: runId,\n });\n while (!isTerminalRun(current) && Date.now() < deadline) {\n await delay(interval, options.signal);\n current = await client.get<Record<string, unknown>>('/api/2.2/jobs/runs/get', {\n run_id: runId,\n });\n }\n return current;\n}\n\nexport async function waitForDatabricksStatement(\n client: WorkspaceClientLike,\n statementId: string,\n options: WorkspaceWaitOptions = {},\n): Promise<Record<string, unknown>> {\n const deadline = Date.now() + (options.timeoutMs ?? 300_000);\n const interval = options.pollIntervalMs ?? 2_000;\n const path = `/api/2.0/sql/statements/${encodeURIComponent(statementId)}`;\n let current = await client.get<Record<string, unknown>>(path);\n while (!isTerminalStatement(current) && Date.now() < deadline) {\n await delay(interval, options.signal);\n current = await client.get<Record<string, unknown>>(path);\n }\n return current;\n}\n\nfunction workspaceOrigin(host: string): string {\n const value = host.trim();\n if (!value) throw new Error('databricks: workspace host cannot be empty');\n const url = new URL(value.includes('://') ? value : `https://${value}`);\n if (url.username || url.password) {\n throw new Error('databricks: workspace host must not contain credentials');\n }\n return url.origin;\n}\n\nfunction isRetryableStatus(status: number): boolean {\n return status === 429 || status === 499 || (status >= 500 && status <= 599);\n}\n\nfunction retryDelayMs(response: Response, attempt: number, base: number): number {\n const retryAfterHeader = response.headers.get('retry-after');\n const retryAfter = retryAfterHeader === null ? Number.NaN : Number(retryAfterHeader);\n return Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter * 1000 : base * 2 ** attempt;\n}\n\nfunction isTerminalRun(run: Record<string, unknown>): boolean {\n const state = run.state as { life_cycle_state?: string } | undefined;\n return ['TERMINATED', 'SKIPPED', 'INTERNAL_ERROR'].includes(state?.life_cycle_state ?? '');\n}\n\nfunction isTerminalStatement(statement: Record<string, unknown>): boolean {\n const status = statement.status as { state?: string } | undefined;\n return ['SUCCEEDED', 'FAILED', 'CANCELED', 'CLOSED'].includes(status?.state ?? '');\n}\n\nfunction delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));\n if (signal.aborted) {\n return Promise.reject(\n signal.reason ?? new DOMException('The operation was aborted', 'AbortError'),\n );\n }\n return new Promise((resolve, reject) => {\n const timer = setTimeout(done, ms);\n signal.addEventListener('abort', aborted, { once: true });\n function done() {\n signal?.removeEventListener('abort', aborted);\n resolve();\n }\n function aborted() {\n clearTimeout(timer);\n reject(signal?.reason ?? new DOMException('The operation was aborted', 'AbortError'));\n }\n });\n}\n","import { readFile } from 'node:fs/promises';\nimport type { ExecutionContext } from './context.js';\nimport { type TableFixture, parseFixtureColumns, resolveFixtureRows } from './fixtures.js';\nimport {\n type WorkspaceClientLike,\n WorkspaceRestClient,\n waitForDatabricksStatement,\n} from './workspace-client.js';\nexport type { WorkspaceClientLike } from './workspace-client.js';\n\nexport interface WorkspaceContextOptions {\n env?: Record<string, string | undefined>;\n /** Schema within the catalog used by qual(); defaults to DBX_TEST_SCHEMA. */\n schema?: string;\n /** Injectable client for hermetic tests; defaults to the testkit workspace client. */\n client?: WorkspaceClientLike;\n /** Injectable fetch for the OAuth M2M exchange. */\n fetchImpl?: typeof fetch;\n pollIntervalMs?: number;\n timeoutMs?: number;\n /** Drop tables seeded by this context on close. Defaults to true. */\n cleanupFixtures?: boolean;\n /** Observe submitted statements for redacted failure evidence. */\n onStatement?: (statement: { sql: string; statementId?: string }) => void;\n}\n\ninterface StatementResponse {\n statement_id?: string;\n status?: { state?: string; error?: { message?: string } };\n manifest?: { schema?: { columns?: { name: string; type_name?: string; position?: number }[] } };\n result?: { data_array?: (string | null)[][] };\n}\n\n/**\n * Live execution context on a Databricks SQL warehouse via the SQL Statement\n * Execution API (/api/2.0/sql/statements). The Statement Execution API takes\n * NAMED parameter markers, so positional `?` params are rewritten to\n * `:p1..:pn` — the ExecutionContext surface stays identical to the local\n * profile.\n */\nexport async function createWorkspaceContext(\n options: WorkspaceContextOptions = {},\n): Promise<ExecutionContext> {\n const env = options.env ?? process.env;\n const warehouseId = resolveWarehouseId(env);\n const catalog = env.DBX_TEST_CATALOG;\n const schema = options.schema ?? env.DBX_TEST_SCHEMA;\n const client = options.client ?? createClientFromEnv(env, options.fetchImpl);\n const fixtureTables = new Set<string>();\n\n const qual = (table: string) =>\n [catalog, schema, table].filter((part): part is string => Boolean(part)).join('.');\n\n async function run(sql: string, params?: readonly unknown[]): Promise<StatementResponse> {\n const { statement, parameters } = toNamedParameters(sql, params ?? []);\n const submitted = await client.post<StatementResponse>('/api/2.0/sql/statements', {\n statement,\n warehouse_id: warehouseId,\n ...(catalog ? { catalog } : {}),\n ...(schema ? { schema } : {}),\n ...(parameters.length > 0 ? { parameters } : {}),\n wait_timeout: '30s',\n on_wait_timeout: 'CONTINUE',\n });\n options.onStatement?.({\n sql,\n ...(submitted.statement_id ? { statementId: submitted.statement_id } : {}),\n });\n const state = submitted.status?.state;\n let terminal = submitted;\n if (state !== 'SUCCEEDED' && state !== 'FAILED' && state !== 'CANCELED' && state !== 'CLOSED') {\n terminal = (await waitForDatabricksStatement(client, submitted.statement_id ?? '', {\n pollIntervalMs: options.pollIntervalMs,\n timeoutMs: options.timeoutMs,\n })) as StatementResponse;\n }\n if (terminal.status?.state !== 'SUCCEEDED') {\n throw new Error(\n `Databricks statement ${terminal.status?.state ?? 'UNKNOWN'}: ${\n terminal.status?.error?.message ?? sql.slice(0, 200)\n }`,\n );\n }\n return terminal;\n }\n\n return {\n profile: 'live',\n qual,\n async exec(sql, params) {\n await run(sql, params);\n },\n async query(sql, params) {\n const response = await run(sql, params);\n return parseStatementRows(response);\n },\n async loadFixture(fixture: TableFixture) {\n if (!schema) {\n throw new Error(\n 'Live fixtures require DBX_TEST_SCHEMA (or a schema option) so tests cannot replace unscoped tables',\n );\n }\n if (!fixture.schema) {\n throw new Error(\n `Live fixtures require explicit column DDL for ${fixture.table} (schema inference is local-only)`,\n );\n }\n const columns = parseFixtureColumns(fixture.schema);\n const ddl = columns.map((c) => `\\`${c.name}\\` ${c.type}`).join(', ');\n const target = qual(fixture.table);\n await run(`CREATE OR REPLACE TABLE ${target} (${ddl}) USING DELTA`);\n fixtureTables.add(target);\n const rows = await resolveFixtureRows(fixture, columns);\n for (const row of rows) {\n const placeholders = columns\n .map((c) => (/TIMESTAMP/i.test(c.type) ? 'CAST(? AS TIMESTAMP)' : '?'))\n .join(', ');\n await run(`INSERT INTO ${qual(fixture.table)} VALUES (${placeholders})`, row);\n }\n },\n async close() {\n if (options.cleanupFixtures !== false && env.DBX_TEST_KEEP_FIXTURES !== '1') {\n for (const table of [...fixtureTables].reverse()) {\n await run(`DROP TABLE IF EXISTS ${table}`);\n }\n }\n fixtureTables.clear();\n },\n };\n}\n\n/** Rewrite positional `?` markers to the named `:pN` markers the Statement Execution API requires. */\nexport function toNamedParameters(\n sql: string,\n params: readonly unknown[],\n): { statement: string; parameters: { name: string; value: string | null }[] } {\n let out = '';\n let index = 0;\n let quote: string | null = null;\n for (let i = 0; i < sql.length; i++) {\n const ch = sql[i] as string;\n if (quote) {\n out += ch;\n if (ch === quote) quote = null;\n continue;\n }\n if (ch === \"'\" || ch === '\"' || ch === '`') {\n quote = ch;\n out += ch;\n continue;\n }\n if (ch === '?') {\n index += 1;\n out += `:p${index}`;\n continue;\n }\n out += ch;\n }\n if (index !== params.length) {\n throw new Error(`SQL has ${index} placeholders but ${params.length} params were provided`);\n }\n return {\n statement: out,\n parameters: params.map((value, i) => ({\n name: `p${i + 1}`,\n value: value === null || value === undefined ? null : String(value),\n })),\n };\n}\n\n/** Map a terminal statement response to normalized row objects. */\nexport function parseStatementRows(response: StatementResponse): Record<string, unknown>[] {\n const columns = [...(response.manifest?.schema?.columns ?? [])].sort(\n (a, b) => (a.position ?? 0) - (b.position ?? 0),\n );\n const data = response.result?.data_array ?? [];\n return data.map((tuple) => {\n const row: Record<string, unknown> = {};\n columns.forEach((column, i) => {\n row[column.name] = convertValue(tuple[i] ?? null, column.type_name ?? 'STRING');\n });\n return row;\n });\n}\n\nconst NUMERIC_TYPES = /^(INT|LONG|SHORT|BYTE|FLOAT|DOUBLE|DECIMAL)/i;\n\nfunction convertValue(value: string | null, typeName: string): unknown {\n if (value === null) return null;\n if (NUMERIC_TYPES.test(typeName)) return Number(value);\n if (/^BOOLEAN/i.test(typeName)) return value === 'true';\n return value;\n}\n\nfunction resolveWarehouseId(env: Record<string, string | undefined>): string {\n if (env.DATABRICKS_WAREHOUSE_ID) return env.DATABRICKS_WAREHOUSE_ID;\n const httpPath = env.DATABRICKS_HTTP_PATH;\n const match = httpPath?.match(/\\/warehouses\\/([\\w-]+)/);\n if (match?.[1]) return match[1];\n throw new Error(\n 'Live profile requires DATABRICKS_WAREHOUSE_ID or DATABRICKS_HTTP_PATH (/sql/1.0/warehouses/<id>)',\n );\n}\n\n/**\n * Auth precedence (ADR-0006): DATABRICKS_BEARER (pre-minted, CI) →\n * explicit env/file OIDC federation → DATABRICKS_CLIENT_ID/SECRET (OAuth\n * M2M) → DATABRICKS_TOKEN (legacy PAT).\n */\nexport function createClientFromEnv(\n env: Record<string, string | undefined>,\n fetchImpl?: typeof fetch,\n): WorkspaceClientLike {\n const host = env.DATABRICKS_HOST;\n if (!host) throw new Error('Live profile requires DATABRICKS_HOST');\n return new WorkspaceRestClient({\n host,\n token: resolveTokenProvider(host, env, fetchImpl),\n ...(fetchImpl ? { fetchImpl } : {}),\n });\n}\n\nexport function resolveTokenProvider(\n host: string,\n env: Record<string, string | undefined>,\n fetchImpl: typeof fetch = fetch,\n): string | (() => Promise<string>) {\n if (env.DATABRICKS_BEARER) return env.DATABRICKS_BEARER;\n const authType = env.DATABRICKS_AUTH_TYPE;\n if (authType === 'env-oidc' || authType === 'file-oidc') {\n let cached: { token: string; expiresAt: number } | null = null;\n return async () => {\n if (cached && Date.now() < cached.expiresAt) return cached.token;\n const subjectToken = await resolveFederatedSubjectToken(authType, env);\n const params = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',\n subject_token: subjectToken,\n subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',\n scope: env.DATABRICKS_SCOPE ?? 'all-apis',\n });\n if (env.DATABRICKS_CLIENT_ID) params.set('client_id', env.DATABRICKS_CLIENT_ID);\n const response = await fetchImpl(`${host.replace(/\\/$/, '')}/oidc/v1/token`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: params.toString(),\n });\n if (!response.ok) {\n throw new Error(`OAuth federated token exchange failed: ${response.status}`);\n }\n const body = (await response.json()) as { access_token?: unknown; expires_in?: unknown };\n if (typeof body.access_token !== 'string' || !body.access_token) {\n throw new Error('OAuth federated token exchange returned no access_token');\n }\n const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : 3600;\n cached = {\n token: body.access_token,\n expiresAt: Date.now() + Math.max(1, expiresIn - 300) * 1000,\n };\n return body.access_token;\n };\n }\n const clientId = env.DATABRICKS_CLIENT_ID;\n const clientSecret = env.DATABRICKS_CLIENT_SECRET;\n if (clientId && clientSecret) {\n let cached: { token: string; expiresAt: number } | null = null;\n return async () => {\n if (cached && Date.now() < cached.expiresAt) return cached.token;\n const response = await fetchImpl(`${host.replace(/\\/$/, '')}/oidc/v1/token`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,\n },\n body: new URLSearchParams({\n grant_type: 'client_credentials',\n scope: env.DATABRICKS_SCOPE ?? 'all-apis',\n }).toString(),\n });\n if (!response.ok) {\n throw new Error(`OAuth M2M token exchange failed: ${response.status}`);\n }\n const body = (await response.json()) as { access_token: string; expires_in?: number };\n cached = {\n token: body.access_token,\n expiresAt: Date.now() + ((body.expires_in ?? 3600) - 300) * 1000,\n };\n return body.access_token;\n };\n }\n if (env.DATABRICKS_TOKEN) return env.DATABRICKS_TOKEN;\n throw new Error(\n 'Live profile requires DATABRICKS_BEARER, env/file OIDC federation, DATABRICKS_CLIENT_ID/SECRET, or DATABRICKS_TOKEN',\n );\n}\n\nasync function resolveFederatedSubjectToken(\n authType: 'env-oidc' | 'file-oidc',\n env: Record<string, string | undefined>,\n): Promise<string> {\n if (authType === 'env-oidc') {\n const tokenEnv = env.DATABRICKS_OIDC_TOKEN_ENV ?? 'DATABRICKS_OIDC_TOKEN';\n const token = env[tokenEnv]?.trim();\n if (!token) throw new Error(`Federated OIDC token environment variable ${tokenEnv} is empty`);\n return token;\n }\n const path = env.DATABRICKS_OIDC_TOKEN_FILEPATH;\n if (!path) throw new Error('DATABRICKS_OIDC_TOKEN_FILEPATH is required for file-oidc');\n const token = (await readFile(path, 'utf8')).trim();\n if (!token) throw new Error(`Federated OIDC token file ${path} is empty`);\n return token;\n}\n"]}