From ebed179748caeb9e92fadbde065b6337da288932 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 9 Nov 2026 16:25:46 +0000 Subject: [PATCH 1/9] test(appkit): reproduce refused and stalled Cloud Fetch downloads Charts on the Arrow path fail with HTTP 500 when workspace storage refuses the app server's Cloud Fetch download, and hang until the route's 120 s first-byte timer fires when storage never answers (LA-2169). Add tests for the behavior the fix needs, all failing against v0.91.0: - Processor: link headers are sent, a 400 is not retried, a 403 re-mints the link once, a wait for headers is bounded and counts as a denial, every download failure carries EXTERNAL_LINK_DOWNLOAD_FAILED. - Delivery: a failed first chunk is replaced by an inline JSON_ARRAY run rebuilt as Arrow, and a result too large for inline is never stranded. - Route: over real HTTP against a loopback storage host, charts render, a definite refusal is remembered for ten minutes, and errors say why. - Rebuild: Arrow rebuilt from positional rows matches the native schema and values for every scalar type, including nulls. Signed-off-by: Pedro Figueiredo --- .../sql-warehouse/tests/arrow-schema.test.ts | 231 ++++++++++++- .../cloud-fetch-fallback.integration.test.ts | 317 +++++++++++++++++ .../analytics/tests/cloud-fetch-fixtures.ts | 219 ++++++++++++ .../analytics/tests/result-delivery.test.ts | 326 +++++++++++++++++- .../tests/arrow-stream-processor.test.ts | 274 +++++++++++++++ 5 files changed, 1364 insertions(+), 3 deletions(-) create mode 100644 packages/appkit/src/plugins/analytics/tests/cloud-fetch-fallback.integration.test.ts create mode 100644 packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts diff --git a/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts b/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts index b7826e87e..65b45a234 100644 --- a/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts +++ b/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts @@ -23,7 +23,11 @@ import { } from "apache-arrow"; import { describe, expect, test } from "vitest"; -import { buildEmptyArrowIPCBase64, parseDatabricksType } from "../arrow-schema"; +import { + buildArrowIPCFromRows, + buildEmptyArrowIPCBase64, + parseDatabricksType, +} from "../arrow-schema"; // ============================================================================ // Helpers @@ -513,3 +517,228 @@ describe("buildEmptyArrowIPCBase64", () => { expect(table.numCols).toBe(0); }); }); + +// ============================================================================ +// buildArrowIPCFromRows (rebuilds ARROW_STREAM from INLINE JSON_ARRAY rows) +// ============================================================================ + +describe("buildArrowIPCFromRows", () => { + type Case = { + column: Parameters[0][number]; + cells: Array; + expected: unknown[]; + }; + + const decimalUnscaled = (v: unknown) => + v === null ? null : String(v as { toString(): string }); + const cases: Record unknown }> = { + INT: { + column: { name: "c", typeName: "INT", typeText: "INT" }, + cells: ["1", null, "-3"], + expected: [1, null, -3], + }, + TINYINT: { + column: { name: "c", typeName: "BYTE", typeText: "TINYINT" }, + cells: ["7", null, "-128"], + expected: [7, null, -128], + }, + SMALLINT: { + column: { name: "c", typeName: "SHORT", typeText: "SMALLINT" }, + cells: ["32767", null], + expected: [32767, null], + }, + BIGINT: { + column: { name: "c", typeName: "LONG", typeText: "BIGINT" }, + cells: ["9007199254740993", null, "-9223372036854775808"], + expected: [9007199254740993n, null, -9223372036854775808n], + }, + FLOAT: { + column: { name: "c", typeName: "FLOAT", typeText: "FLOAT" }, + cells: ["1.5", null, "-0.25"], + expected: [1.5, null, -0.25], + }, + DOUBLE: { + column: { name: "c", typeName: "DOUBLE", typeText: "DOUBLE" }, + cells: ["1.1", "NaN", null], + expected: [1.1, Number.NaN, null], + }, + DECIMAL: { + column: { + name: "c", + typeName: "DECIMAL", + typeText: "DECIMAL(10,2)", + typePrecision: 10, + typeScale: 2, + }, + cells: ["1.50", "-0.05", null, "12345678.99", "7"], + expected: ["150", "-5", null, "1234567899", "700"], + read: decimalUnscaled, + }, + "DECIMAL without (p,s) in typeText": { + column: { + name: "c", + typeName: "DECIMAL", + typeText: "DECIMAL", + typePrecision: 12, + typeScale: 3, + }, + cells: ["1.5", null], + expected: ["1500", null], + read: decimalUnscaled, + }, + DATE: { + column: { name: "c", typeName: "DATE", typeText: "DATE" }, + cells: ["2026-01-02", null], + expected: [Date.UTC(2026, 0, 2), null], + }, + TIMESTAMP: { + column: { name: "c", typeName: "TIMESTAMP", typeText: "TIMESTAMP" }, + cells: ["2026-01-02T10:00:00.123Z", null], + expected: [Date.UTC(2026, 0, 2, 10, 0, 0, 123), null], + }, + TIMESTAMP_NTZ: { + column: { + name: "c", + typeName: "TIMESTAMP_NTZ", + typeText: "TIMESTAMP_NTZ", + }, + cells: ["2026-01-02T10:00:00.123", null], + expected: [Date.UTC(2026, 0, 2, 10, 0, 0, 123), null], + }, + STRING: { + column: { name: "c", typeName: "STRING", typeText: "STRING" }, + cells: ["a", "", null], + expected: ["a", "", null], + }, + BOOLEAN: { + column: { name: "c", typeName: "BOOLEAN", typeText: "BOOLEAN" }, + cells: ["true", "false", null], + expected: [true, false, null], + }, + BINARY: { + column: { name: "c", typeName: "BINARY", typeText: "BINARY" }, + cells: ["AQI=", null], + expected: [[1, 2], null], + read: (v) => (v === null ? null : [...(v as Uint8Array)]), + }, + VOID: { + column: { name: "c", typeName: "NULL", typeText: "VOID" }, + cells: [null, null], + expected: [null, null], + }, + }; + + test.each(Object.entries(cases))( + "%s keeps the native Arrow type and values, nulls included", + (_name, { column, cells, expected, read }) => { + const ipc = buildArrowIPCFromRows( + [column], + cells.map((cell) => [cell]), + ); + const table = tableFromIPC(ipc); + + const native = parseDatabricksType(column.typeText ?? "STRING"); + const field = table.schema.fields[0]; + expect(field.name).toBe("col_0"); + expect(field.typeId).toBe(native.typeId); + if (column.typeName === "DECIMAL") { + const type = field.type as Decimal; + expect([type.precision, type.scale]).toEqual([ + column.typePrecision, + column.typeScale, + ]); + } else { + expect(String(field.type)).toBe(String(native)); + } + const values = [...(table.getChild("col_0") as Iterable)]; + expect(values.map((v) => (read ? read(v) : v))).toEqual(expected); + }, + ); + + test("positional rows keep duplicate column names and JSON-looking strings verbatim", () => { + const payload = '{"id":9007199254740993, "label":"kept verbatim"}'; + const ipc = buildArrowIPCFromRows( + [ + { name: "x", typeName: "INT", typeText: "INT" }, + { name: "x", typeName: "INT", typeText: "INT" }, + { name: "payload", typeName: "STRING", typeText: "STRING" }, + ], + [["1", "2", payload]], + ); + const table = tableFromIPC(ipc); + expect(table.schema.fields.map((f) => f.name)).toEqual([ + "col_0", + "col_1", + "col_2", + ]); + expect(table.getChild("col_0")?.get(0)).toBe(1); + expect(table.getChild("col_1")?.get(0)).toBe(2); + expect(table.getChild("col_2")?.get(0)).toBe(payload); + }); + + test("nested, interval and geospatial columns stay strings", () => { + const columns = [ + { name: "a", typeText: "ARRAY" }, + { name: "m", typeText: "MAP" }, + { name: "s", typeText: "STRUCT" }, + { name: "i", typeText: "INTERVAL DAY" }, + { name: "g", typeName: "GEOGRAPHY", typeText: "GEOGRAPHY(4326)" }, + ]; + const row = [ + "[1,2]", + '{"k":1}', + '{"f":1}', + "1 00:00:00.000000000", + "POINT(1 2)", + ]; + const table = tableFromIPC(buildArrowIPCFromRows(columns, [row])); + expect(table.schema.fields.map((f) => f.typeId)).toEqual( + columns.map(() => Type.Utf8), + ); + expect(table.toArray()[0].toJSON()).toEqual({ + col_0: row[0], + col_1: row[1], + col_2: row[2], + col_3: row[3], + col_4: row[4], + }); + }); + + test("zero rows still yield the typed schema", () => { + const table = tableFromIPC( + buildArrowIPCFromRows( + [{ name: "n", typeName: "LONG", typeText: "BIGINT" }], + [], + ), + ); + expect(table.numRows).toBe(0); + expect(table.schema.fields[0].typeId).toBe(Type.Int); + }); + + test("a cell that does not parse as its column type throws", () => { + expect(() => + buildArrowIPCFromRows( + [{ typeText: "INT" }, { typeText: "DATE" }], + [["1", "not-a-date"]], + ), + ).toThrow(/Invalid date/); + expect(() => + buildArrowIPCFromRows( + [{ typeText: "DECIMAL(5,2)", typePrecision: 5, typeScale: 2 }], + [["abc"]], + ), + ).toThrow(/Invalid decimal/); + expect(() => + buildArrowIPCFromRows([{ typeText: "INT" }], [["abc"]]), + ).toThrow(/Invalid integer/); + expect(() => + buildArrowIPCFromRows([{ typeText: "BIGINT" }], [["1.5"]]), + ).toThrow(); + expect(() => + buildArrowIPCFromRows([{ typeText: "DOUBLE" }], [["x"]]), + ).toThrow(/Invalid number/); + expect(() => + buildArrowIPCFromRows([{ typeText: "BOOLEAN" }], [["maybe"]]), + ).toThrow(/Invalid boolean/); + }); +}); diff --git a/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fallback.integration.test.ts b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fallback.integration.test.ts new file mode 100644 index 000000000..39d1c39dc --- /dev/null +++ b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fallback.integration.test.ts @@ -0,0 +1,317 @@ +import { + createTestApp, + getMock, + type TestApp, +} from "@databricks/appkit/testing"; +import { tableFromIPC } from "apache-arrow"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; + +import { AppManager } from "../../../app"; +import { analytics } from "../index"; +import { + cellsOf, + type Dataset, + type FakeStorage, + schemaOf, + startStorage, + type StorageMode, + typedDataset, +} from "./cloud-fetch-fixtures"; + +const LARGE = 26 * 1024 * 1024; +const TTL_MS = 10 * 60_000; + +const getAppQuerySpy = vi.spyOn(AppManager.prototype, "getAppQuery"); + +type Warehouse = { + executions: string[]; +}; + +describe("Arrow charts when workspace storage refuses the Cloud Fetch download", () => { + let app: TestApp<[ReturnType]>; + let storage: FakeStorage; + let warehouse: Warehouse; + let data: Dataset; + let executeStatement: ReturnType; + let getResultData: ReturnType; + + /** The warehouse behind `statementExecution`, answering like a standard SQL warehouse. */ + function installWarehouse( + options: { + totalByteCount?: number; + inline?: () => never; + } = {}, + ) { + let seq = 0; + executeStatement.mockImplementation(async (body: any) => { + warehouse.executions.push(`${body.disposition}+${body.format}`); + const statementId = `stmt-${++seq}`; + const manifest = (format: string) => ({ + format, + schema: { columns: data.columns }, + totalChunkCount: 1, + totalByteCount: + options.totalByteCount === undefined + ? undefined + : BigInt(options.totalByteCount), + }); + if (body.disposition === "INLINE" && body.format === "ARROW_STREAM") { + throw Object.assign( + new Error( + "The format field must be JSON_ARRAY when the disposition field is INLINE.", + ), + { errorCode: "INVALID_PARAMETER_VALUE" }, + ); + } + if (body.disposition === "EXTERNAL_LINKS") { + return { + statementId, + status: { state: "SUCCEEDED" }, + manifest: manifest("ARROW_STREAM"), + result: { + externalLinks: [ + { + chunkIndex: 0, + externalLink: storage.url("chunk-0"), + httpHeaders: { "x-ms-encryption-key": "k" }, + }, + ], + }, + }; + } + if (options.inline) return options.inline(); + return { + statementId, + status: { state: "SUCCEEDED" }, + manifest: manifest("JSON_ARRAY"), + result: { dataArray: data.dataArray }, + }; + }); + getResultData.mockImplementation(async () => ({ + externalLinks: [ + { + chunkIndex: 0, + externalLink: storage.url("chunk-0-fresh"), + httpHeaders: { "x-ms-encryption-key": "k" }, + }, + ], + })); + } + + async function boot(config: { timeout?: number } = {}) { + app = await createTestApp({ plugins: [analytics(config)] }); + executeStatement = getMock( + app.client, + "statementExecution.executeStatement", + ); + getResultData = getMock(app.client, "statementExecution.getResultData"); + getAppQuerySpy.mockResolvedValue({ + query: "SELECT * FROM sales", + isAsUser: false, + }); + } + + async function setStorage(mode: StorageMode) { + storage = await startStorage(mode); + storage.setBody(data.ipc); + } + + beforeEach(() => { + warehouse = { executions: [] }; + data = typedDataset(); + }); + + afterEach(async () => { + vi.useRealTimers(); + getAppQuerySpy.mockReset(); + await app?.close(); + await storage?.close(); + }); + + const chart = (key: string) => + app.post(`/api/analytics/query/${key}`, { + body: { parameters: {}, format: "ARROW_STREAM" }, + }); + + const columnsHeader = (res: Response) => + JSON.parse( + decodeURIComponent(res.headers.get("X-Appkit-Arrow-Columns") ?? "null"), + ); + + async function expectNativeArrow(res: Response) { + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toBe( + "application/vnd.apache.arrow.stream", + ); + const native = tableFromIPC(data.ipc); + const served = tableFromIPC(new Uint8Array(await res.arrayBuffer())); + expect(schemaOf(served)).toEqual(schemaOf(native)); + expect(cellsOf(served)).toEqual(cellsOf(native)); + expect(columnsHeader(res)).toEqual(data.columns.map((c) => c.name)); + } + + test("healthy storage is untouched by the fallback: same bytes, no inline run, one GET with the link's headers", async () => { + await setStorage({ kind: "ok" }); + await boot(); + installWarehouse(); + + const res = await chart("healthy"); + + expect(res.status).toBe(200); + expect( + Buffer.from(await res.arrayBuffer()).equals(Buffer.from(data.ipc)), + ).toBe(true); + expect(columnsHeader(res)).toEqual(data.columns.map((c) => c.name)); + expect(warehouse.executions).toEqual([ + "INLINE+ARROW_STREAM", + "EXTERNAL_LINKS+ARROW_STREAM", + ]); + expect(storage.gets()).toBe(1); + expect(storage.requestHeaders[0]["x-ms-encryption-key"]).toBe("k"); + }); + + test("a firewall that answers 403 still renders the chart, and the re-minted link is tried once", async () => { + await setStorage({ kind: "status", status: 403 }); + await boot(); + installWarehouse(); + + const res = await chart("firewalled"); + + await expectNativeArrow(res); + expect(warehouse.executions).toEqual([ + "INLINE+ARROW_STREAM", + "EXTERNAL_LINKS+ARROW_STREAM", + "INLINE+JSON_ARRAY", + ]); + expect(storage.gets()).toBe(2); + expect(getResultData).toHaveBeenCalledTimes(1); + }); + + test("a definite refusal is remembered for ten minutes, then storage is tried again", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + await setStorage({ kind: "status", status: 403 }); + await boot(); + installWarehouse(); + + await expectNativeArrow(await chart("first")); + const getsAfterFirst = storage.gets(); + + warehouse.executions.length = 0; + await expectNativeArrow(await chart("second")); + expect(warehouse.executions).toEqual(["INLINE+JSON_ARRAY"]); + expect(storage.gets()).toBe(getsAfterFirst); + + warehouse.executions.length = 0; + await expectNativeArrow(await chart("second")); + expect(warehouse.executions).toEqual([]); + + vi.setSystemTime(Date.now() + TTL_MS + 1000); + storage.setMode({ kind: "ok" }); + warehouse.executions.length = 0; + const recovered = await chart("third"); + expect( + Buffer.from(await recovered.arrayBuffer()).equals(Buffer.from(data.ipc)), + ).toBe(true); + expect(warehouse.executions).toEqual(["EXTERNAL_LINKS+ARROW_STREAM"]); + expect(storage.gets()).toBe(getsAfterFirst + 1); + }); + + test("storage that never answers falls back, and the next chart tries storage again", async () => { + await setStorage({ kind: "hang" }); + await boot({ timeout: 300 }); + installWarehouse(); + + await expectNativeArrow(await chart("stalled")); + expect(storage.gets()).toBe(1); + + warehouse.executions.length = 0; + await expectNativeArrow(await chart("stalled-again")); + expect(warehouse.executions).toContain("EXTERNAL_LINKS+ARROW_STREAM"); + expect(storage.gets()).toBe(2); + }); + + test("a body that stalls after the headers falls back instead of hanging", async () => { + await setStorage({ kind: "hang-after-headers" }); + await boot({ timeout: 300 }); + installWarehouse(); + + await expectNativeArrow(await chart("stalled-body")); + expect(warehouse.executions.at(-1)).toBe("INLINE+JSON_ARRAY"); + }); + + test("duplicate column names and JSON-looking strings come through the fallback intact", async () => { + const payload = '{"id":9007199254740993, "label":"kept verbatim"}'; + data = { + columns: [ + { name: "x", typeName: "INT", typeText: "INT", position: 0 }, + { name: "x", typeName: "INT", typeText: "INT", position: 1 }, + { + name: "payload", + typeName: "STRING", + typeText: "STRING", + position: 2, + }, + ], + dataArray: [["1", "2", payload]], + ipc: new Uint8Array(), + }; + await setStorage({ kind: "status", status: 403 }); + await boot(); + installWarehouse(); + + const res = await chart("duplicates"); + + expect(res.status).toBe(200); + const served = tableFromIPC(new Uint8Array(await res.arrayBuffer())); + expect(cellsOf(served)).toEqual([["1", "2", payload]]); + expect(columnsHeader(res)).toEqual(["x", "x", "payload"]); + }); + + test("a refused download of a result too large for inline answers with EXTERNAL_LINKS_DENIED and says why", async () => { + await setStorage({ kind: "status", status: 403 }); + await boot(); + installWarehouse({ totalByteCount: LARGE }); + + const res = await chart("large"); + + expect(res.status).toBe(500); + const body = (await res.json()) as { error: string; errorCode: string }; + expect(body.errorCode).toBe("EXTERNAL_LINKS_DENIED"); + expect(body.error).toMatch(/workspace storage/); + expect(body.error).toMatch(/too large to send inline/); + expect(warehouse.executions).not.toContain("INLINE+JSON_ARRAY"); + }); + + test("a large result behind storage that never answers is tried twice, then says why", async () => { + await setStorage({ kind: "hang" }); + await boot({ timeout: 300 }); + installWarehouse({ totalByteCount: LARGE }); + + const res = await chart("large-stalled"); + + expect(res.status).toBe(500); + const body = (await res.json()) as { errorCode: string }; + expect(body.errorCode).toBe("EXTERNAL_LINKS_DENIED"); + expect(storage.gets()).toBe(2); + expect(warehouse.executions).not.toContain("INLINE+JSON_ARRAY"); + }); + + test("when the inline run fails for another reason the user sees that reason, not a storage message", async () => { + await setStorage({ kind: "status", status: 403 }); + await boot(); + installWarehouse({ + inline: () => { + throw Object.assign(new Error("User does not have SELECT"), { + errorCode: "PERMISSION_DENIED", + }); + }, + }); + + const res = await chart("permission"); + + expect(res.status).toBe(500); + const body = (await res.json()) as { error: string; errorCode: string }; + expect(body.errorCode).toBe("PERMISSION_DENIED"); + expect(body.error).not.toMatch(/storage|firewall/i); + }); +}); diff --git a/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts new file mode 100644 index 000000000..d6431a1c9 --- /dev/null +++ b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts @@ -0,0 +1,219 @@ +import { createServer, type IncomingMessage, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; + +import { + Bool, + DateDay, + Decimal, + Float64, + Int32, + Int64, + makeData, + makeVector, + type Table, + Table as ArrowTable, + TimestampMicrosecond, + tableToIPC, + Utf8, + vectorFromArray, +} from "apache-arrow"; + +/** A standard SQL warehouse's view of one result: manifest columns, JSON rows, native Arrow. */ +export interface Dataset { + columns: Array<{ + name: string; + typeName: string; + typeText: string; + position: number; + typePrecision?: number; + typeScale?: number; + }>; + /** Positional string rows, exactly as INLINE + JSON_ARRAY returns them. */ + dataArray: Array>; + /** The IPC stream Cloud Fetch serves for the same result. */ + ipc: Uint8Array; +} + +const DAY = 86_400_000; +const isNull = (row: number, col: number) => (row + col) % 7 === 3; + +function decimalVector(unscaled: Array, scale: number) { + const words = new Uint32Array(unscaled.length * 4); + const validity = new Uint8Array(Math.ceil(unscaled.length / 8)); + let nullCount = 0; + unscaled.forEach((v, i) => { + if (v === null) { + nullCount++; + return; + } + validity[i >> 3] |= 1 << (i & 7); + const twos = v < 0n ? (1n << 128n) + v : v; + for (let w = 0; w < 4; w++) { + words[i * 4 + w] = Number((twos >> BigInt(32 * w)) & 0xffffffffn); + } + }); + return makeVector( + makeData({ + type: new Decimal(scale, 10, 128), + length: unscaled.length, + nullCount, + nullBitmap: validity, + data: words, + }), + ); +} + +const fixed = (v: bigint) => { + const sign = v < 0n ? "-" : ""; + const digits = (v < 0n ? -v : v).toString().padStart(3, "0"); + return `${sign}${digits.slice(0, -2)}.${digits.slice(-2)}`; +}; + +/** Eight scalar types with nulls sprinkled through every column. */ +export function typedDataset(rows = 20): Dataset { + const col = (c: number, f: (i: number) => T) => + Array.from({ length: rows }, (_, i) => (isNull(i, c) ? null : f(i))); + const id = col(0, (i) => i); + const big = col(1, (i) => 9007199254740993n + BigInt(i)); + const amount = col(2, (i) => i * 1.5); + const price = col(3, (i) => BigInt(i * 101 - 50)); + const day = col(4, (i) => Date.UTC(2026, 0, 1) + i * DAY); + const ts = col(5, (i) => Date.UTC(2026, 0, 1, 10) + i * 3_600_000); + const label = col(6, (i) => `row-${i}`); + const flag = col(7, (i) => i % 2 === 0); + + const table = new ArrowTable({ + col_0: vectorFromArray(id, new Int32()), + col_1: vectorFromArray(big, new Int64()), + col_2: vectorFromArray(amount, new Float64()), + col_3: decimalVector(price, 2), + col_4: vectorFromArray(day, new DateDay()), + col_5: vectorFromArray(ts, new TimestampMicrosecond("UTC")), + col_6: vectorFromArray(label, new Utf8()), + col_7: vectorFromArray(flag, new Bool()), + }); + + const text = (v: unknown, render: (x: never) => string) => + v === null ? null : render(v as never); + const dataArray = Array.from({ length: rows }, (_, i) => [ + text(id[i], String), + text(big[i], String), + text(amount[i], String), + text(price[i], fixed), + text(day[i], (x: number) => new Date(x).toISOString().slice(0, 10)), + text(ts[i], (x: number) => new Date(x).toISOString()), + text(label[i], String), + text(flag[i], String), + ]); + + const column = ( + name: string, + typeName: string, + typeText: string, + position: number, + extra: { typePrecision?: number; typeScale?: number } = {}, + ) => ({ name, typeName, typeText, position, ...extra }); + return { + columns: [ + column("id", "INT", "INT", 0), + column("big", "LONG", "BIGINT", 1), + column("amount", "DOUBLE", "DOUBLE", 2), + column("price", "DECIMAL", "DECIMAL(10,2)", 3, { + typePrecision: 10, + typeScale: 2, + }), + column("day", "DATE", "DATE", 4), + column("ts", "TIMESTAMP", "TIMESTAMP", 5), + column("label", "STRING", "STRING", 6), + column("flag", "BOOLEAN", "BOOLEAN", 7), + ], + dataArray, + ipc: tableToIPC(table, "stream"), + }; +} + +/** Every cell as a string, so schemas and values compare across Arrow builds. */ +export function cellsOf(table: Table): Array> { + const columns = table.schema.fields.map((f) => + [...(table.getChild(f.name) as Iterable)].map((v) => + v === null || v === undefined ? null : String(v), + ), + ); + return Array.from({ length: table.numRows }, (_, i) => + columns.map((c) => c[i]), + ); +} + +export function schemaOf(table: Table): string[] { + return table.schema.fields.map((f) => `${f.name}:${String(f.type)}`); +} + +export type StorageMode = + | { kind: "ok"; delayMs?: number } + | { kind: "status"; status: number } + | { kind: "hang" } + | { kind: "hang-after-headers" } + | { kind: "reset-after-headers" }; + +export interface FakeStorage { + /** GETs received, whatever the answer. */ + readonly gets: () => number; + readonly requestHeaders: IncomingMessage["headers"][]; + url(path?: string): string; + setMode(mode: StorageMode): void; + /** Body served on a healthy GET. */ + setBody(bytes: Uint8Array): void; + close(): Promise; +} + +/** A loopback host standing in for workspace storage behind a presigned URL. */ +export async function startStorage( + initial: StorageMode = { kind: "ok" }, +): Promise { + let mode = initial; + let body: Uint8Array = new Uint8Array(); + let gets = 0; + const requestHeaders: IncomingMessage["headers"][] = []; + const server: Server = createServer((req, res) => { + gets++; + requestHeaders.push(req.headers); + const current = mode; + switch (current.kind) { + case "ok": + setTimeout(() => res.end(Buffer.from(body)), current.delayMs ?? 0); + return; + case "status": + res.writeHead(current.status).end("denied"); + return; + case "hang": + return; + case "hang-after-headers": + res.writeHead(200, { "content-length": 100_000 }); + res.flushHeaders(); + return; + case "reset-after-headers": + res.writeHead(200, { "content-length": 100_000 }); + res.flushHeaders(); + setTimeout(() => req.socket.destroy(), 30); + return; + } + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; + return { + gets: () => gets, + requestHeaders, + url: (path = "chunk") => `${base}/${path}`, + setMode: (next) => { + mode = next; + }, + setBody: (bytes) => { + body = bytes; + }, + close: () => + new Promise((resolve) => { + server.closeAllConnections(); + server.close(() => resolve()); + }), + }; +} diff --git a/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts b/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts index 06438b5a5..14025074e 100644 --- a/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts @@ -1,16 +1,34 @@ -import { Table, tableToIPC, Utf8, vectorFromArray } from "apache-arrow"; -import { describe, expect, test, vi } from "vitest"; +import { + Table, + tableFromIPC, + tableToIPC, + Utf8, + vectorFromArray, +} from "apache-arrow"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { ExecutionError } from "../../../errors"; +import { ArrowStreamProcessor } from "../../../stream/arrow-stream-processor"; import { type ArrowChunkStreamer, + type ArrowDeliveryOut, + type ArrowCapability, arrowDeliveryUnsupported, classifyDispositionRejection, decodeArrowAttachmentToRows, deliverArrowBytes, + deliverArrowBytesWithFallback, deliverJsonResult, type QueryExecutor, } from "../result-delivery"; +import { + cellsOf, + type Dataset, + type FakeStorage, + schemaOf, + startStorage, + typedDataset, +} from "./cloud-fetch-fixtures"; /** Build a base64 Arrow IPC stream (2 cols, given names + rows). */ function arrowBase64(names: [string, string]): string { @@ -170,6 +188,7 @@ describe("deliverArrowBytes — normal warehouse (fallback to EXTERNAL_LINKS)", links, undefined, undefined, + false, ); expect(out.columnNames).toEqual(["a", "b"]); expect(chunks).toEqual([extBytes]); @@ -424,3 +443,306 @@ describe("decodeArrowAttachmentToRows — captured Reyden fixtures", () => { expect(rows).toEqual([REYDEN_NESTED_FIXTURE.expected]); }); }); + +describe("deliverArrowBytesWithFallback", () => { + const LARGE = 26 * 1024 * 1024; + const storages: FakeStorage[] = []; + + afterEach(async () => { + await Promise.all(storages.splice(0).map((st) => st.close())); + }); + + async function setup( + options: { + storage?: Parameters[0]; + totalByteCount?: number; + inline?: () => unknown; + rows?: number; + } = {}, + ) { + const data: Dataset = typedDataset(options.rows); + const storage = await startStorage(options.storage); + storage.setBody(data.ipc); + storages.push(storage); + const executions: string[] = []; + const executor: QueryExecutor = { + query: async (_q, _p, fp) => { + executions.push(`${fp.disposition}+${fp.format}`); + if (fp.disposition === "INLINE" && fp.format === "ARROW_STREAM") { + throw ExecutionError.statementFailed( + "The format field must be JSON_ARRAY when the disposition field is INLINE.", + "INVALID_PARAMETER_VALUE", + ); + } + if (fp.disposition === "EXTERNAL_LINKS") { + return { + statement_id: "stmt-links", + columnNames: data.columns.map((c) => c.name), + totalByteCount: options.totalByteCount, + external_links: [{ chunkIndex: 0, externalLink: storage.url() }], + }; + } + if (options.inline) return options.inline() as never; + return { + statement_id: "stmt-json", + columns: data.columns, + columnNames: data.columns.map((c) => c.name), + dataArray: data.dataArray, + }; + }, + }; + const processor = new ArrowStreamProcessor({ + timeout: 1000, + retries: 2, + responseTimeoutMs: 100, + }); + const streamer: ArrowChunkStreamer = { + streamExternalLinks: (links, signal, refresh, patient) => + processor.streamChunks(links, signal, refresh, patient), + }; + return { data, storage, executions, executor, streamer }; + } + + async function run( + ctx: Awaited>, + capabilityHint?: ArrowCapability, + ) { + const out: ArrowDeliveryOut = {}; + const reported: ArrowCapability[] = []; + const chunks: Uint8Array[] = []; + let error: ExecutionError | undefined; + try { + for await (const bytes of deliverArrowBytesWithFallback( + ctx.executor, + ctx.streamer, + "SELECT 1", + undefined, + out, + undefined, + { + capabilityHint, + onCapabilityResolved: (c) => reported.push(c), + }, + )) { + chunks.push(bytes); + } + } catch (e) { + error = e as ExecutionError; + } + const bytes = Buffer.concat(chunks.map((c) => Buffer.from(c))); + return { bytes, out, reported, error }; + } + + const expectNativeArrow = (ctx: { data: Dataset }, bytes: Uint8Array) => { + const native = tableFromIPC(ctx.data.ipc); + const served = tableFromIPC(new Uint8Array(bytes)); + expect(schemaOf(served)).toEqual(schemaOf(native)); + expect(cellsOf(served)).toEqual(cellsOf(native)); + }; + + test("a healthy workspace streams the download and never runs the inline fallback", async () => { + const ctx = await setup(); + const { bytes, reported, error } = await run(ctx); + + expect(error).toBeUndefined(); + expect(Buffer.from(ctx.data.ipc).equals(bytes)).toBe(true); + expect(ctx.executions).toEqual([ + "INLINE+ARROW_STREAM", + "EXTERNAL_LINKS+ARROW_STREAM", + ]); + expect(ctx.storage.gets()).toBe(1); + expect(reported).toEqual(["external"]); + }); + + test.each([403, 400])( + "a %i from storage serves rebuilt Arrow equal to the native result and reports links-denied", + async (status) => { + const ctx = await setup({ storage: { kind: "status", status } }); + const { bytes, out, reported, error } = await run(ctx); + + expect(error).toBeUndefined(); + expectNativeArrow(ctx, bytes); + expect(out.columnNames).toEqual(ctx.data.columns.map((c) => c.name)); + expect(out.statementId).toBe("stmt-json"); + expect(ctx.executions).toEqual([ + "INLINE+ARROW_STREAM", + "EXTERNAL_LINKS+ARROW_STREAM", + "INLINE+JSON_ARRAY", + ]); + expect(reported.at(-1)).toBe("links-denied"); + }, + ); + + test.each([ + ["no answer", { kind: "hang" }], + ["headers then no body", { kind: "hang-after-headers" }], + ["headers then a reset", { kind: "reset-after-headers" }], + ] as const)( + "storage with %s falls back once and is not remembered as denied", + async (_name, storage) => { + const ctx = await setup({ storage }); + const { bytes, reported, error } = await run(ctx); + + expect(error).toBeUndefined(); + expectNativeArrow(ctx, bytes); + expect(ctx.storage.gets()).toBe(1); + expect(ctx.executions.at(-1)).toBe("INLINE+JSON_ARRAY"); + expect(reported).not.toContain("links-denied"); + }, + ); + + test("with the links-denied hint the query goes inline and storage is not touched", async () => { + const ctx = await setup({ storage: { kind: "status", status: 403 } }); + const { bytes, error } = await run(ctx, "links-denied"); + + expect(error).toBeUndefined(); + expectNativeArrow(ctx, bytes); + expect(ctx.executions).toEqual(["INLINE+JSON_ARRAY"]); + expect(ctx.storage.gets()).toBe(0); + }); + + test("a refusal that is followed by an unrelated inline failure surfaces that failure unchanged", async () => { + const ctx = await setup({ + storage: { kind: "status", status: 403 }, + inline: () => { + throw ExecutionError.statementFailed( + "User does not have SELECT on catalog.schema.table", + "PERMISSION_DENIED", + "You do not have permission to run this query.", + ); + }, + }); + const { error } = await run(ctx); + + expect(error?.errorCode).toBe("PERMISSION_DENIED"); + expect(error?.clientMessage).toBe( + "You do not have permission to run this query.", + ); + }); + + test("a refused download of a result too large for inline fails saying why, without running inline", async () => { + const ctx = await setup({ + storage: { kind: "status", status: 403 }, + totalByteCount: LARGE, + }); + const { error, reported } = await run(ctx); + + expect(error?.errorCode).toBe("EXTERNAL_LINKS_DENIED"); + expect(error?.clientMessage).toMatch(/workspace storage/); + expect(error?.clientMessage).toMatch(/too large to send inline/); + expect(error?.clientMessage).not.toMatch(/azure/i); + expect(ctx.executions).toEqual([ + "INLINE+ARROW_STREAM", + "EXTERNAL_LINKS+ARROW_STREAM", + ]); + expect(reported.at(-1)).toBe("links-denied"); + }); + + test("a slow first byte on a large result gets one patient pass instead of a denial", async () => { + const ctx = await setup({ + storage: { kind: "ok", delayMs: 300 }, + totalByteCount: LARGE, + }); + const { bytes, reported, error } = await run(ctx); + + expect(error).toBeUndefined(); + expect(Buffer.from(ctx.data.ipc).equals(bytes)).toBe(true); + expect(ctx.storage.gets()).toBe(2); + expect(ctx.executions).not.toContain("INLINE+JSON_ARRAY"); + expect(reported).not.toContain("links-denied"); + }); + + test("a large result whose storage never answers fails saying why after the patient pass", async () => { + const ctx = await setup({ + storage: { kind: "hang" }, + totalByteCount: LARGE, + }); + const { error, reported } = await run(ctx); + + expect(error?.errorCode).toBe("EXTERNAL_LINKS_DENIED"); + expect(ctx.storage.gets()).toBe(2); + expect(ctx.executions).not.toContain("INLINE+JSON_ARRAY"); + expect(reported).not.toContain("links-denied"); + }); + + test("an inline result that spans several chunks is not served truncated", async () => { + const ctx = await setup({ + storage: { kind: "status", status: 403 }, + inline: () => ({ + statement_id: "stmt-json", + columns: typedDataset().columns, + dataArray: typedDataset().dataArray.slice(0, 3), + nextChunkIndex: 1, + }), + }); + const { error } = await run(ctx); + + expect(error?.errorCode).toBe("EXTERNAL_LINKS_DENIED"); + }); + + test("with the links-denied hint, a result too big for inline still downloads once storage recovers", async () => { + const ctx = await setup({ + inline: () => ({ + statement_id: "stmt-json", + columns: typedDataset().columns, + dataArray: [], + nextChunkIndex: 1, + }), + }); + const { bytes, reported, error } = await run(ctx, "links-denied"); + + expect(error).toBeUndefined(); + expect(Buffer.from(ctx.data.ipc).equals(bytes)).toBe(true); + expect(ctx.executions).toEqual([ + "INLINE+JSON_ARRAY", + "EXTERNAL_LINKS+ARROW_STREAM", + ]); + expect(ctx.storage.gets()).toBe(1); + expect(reported.at(-1)).toBe("external"); + }); + + test("with the links-denied hint, a too-big result while storage still refuses fails saying why", async () => { + const ctx = await setup({ + storage: { kind: "status", status: 403 }, + inline: () => ({ + statement_id: "stmt-json", + columns: typedDataset().columns, + dataArray: [], + nextChunkIndex: 1, + }), + }); + const { error } = await run(ctx, "links-denied"); + + expect(error?.errorCode).toBe("EXTERNAL_LINKS_DENIED"); + }); + + test("with the links-denied hint, an unrelated inline failure is what the user sees when storage still refuses", async () => { + const ctx = await setup({ + storage: { kind: "status", status: 403 }, + inline: () => { + throw ExecutionError.statementFailed( + "warehouse is restarting", + "TEMPORARILY_UNAVAILABLE", + ); + }, + }); + const { error } = await run(ctx, "links-denied"); + + expect(error?.errorCode).toBe("TEMPORARILY_UNAVAILABLE"); + }); + + test("a result the rebuild cannot parse fails with a code and a message, not a generic error", async () => { + const ctx = await setup({ + storage: { kind: "status", status: 403 }, + inline: () => ({ + statement_id: "stmt-json", + columns: [{ name: "n", typeName: "INT", typeText: "INT" }], + dataArray: [["not-a-number"]], + }), + }); + const { error } = await run(ctx); + + expect(error?.errorCode).toBe("ARROW_REBUILD_FAILED"); + expect(error?.clientMessage).toMatch(/JSON_ARRAY/); + }); +}); diff --git a/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts b/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts index 84d1ee031..2d2caf623 100644 --- a/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts +++ b/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts @@ -1,7 +1,13 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; + import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { ExecutionError } from "../../errors"; import { ArrowStreamProcessor } from "../arrow-stream-processor"; +const EXTERNAL_LINK_DOWNLOAD_FAILED = "EXTERNAL_LINK_DOWNLOAD_FAILED"; + /** A ReadableStream that emits the given pieces in order, then closes. */ function streamOf(...pieces: Uint8Array[]): ReadableStream { return new ReadableStream({ @@ -264,3 +270,271 @@ describe("ArrowStreamProcessor.streamChunks", () => { expect(bytes).toEqual([7]); }, 10000); }); + +const response = (status: number, statusText = "") => + ({ ok: false, status, statusText }) as Response; +const okResponse = (...pieces: number[]) => + ({ + ok: true, + status: 200, + body: streamOf(new Uint8Array(pieces)), + }) as unknown as Response; + +async function failureOf(gen: AsyncGenerator) { + try { + await drain(gen); + } catch (error) { + return error as ExecutionError; + } + throw new Error("expected the download to fail"); +} + +describe("ArrowStreamProcessor — refused downloads", () => { + let originalFetch: typeof globalThis.fetch; + + beforeEach(() => { + originalFetch = globalThis.fetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + test("sends the link's httpHeaders on every GET, and the new link's headers after a re-mint", async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce(response(403, "Forbidden")) + .mockResolvedValueOnce(okResponse(1)); + globalThis.fetch = fetchMock; + const refresh = vi.fn(async (chunkIndex: number) => ({ + chunkIndex, + externalLink: "https://example.com/fresh", + httpHeaders: { "x-key": "fresh" }, + })); + + const p = new ArrowStreamProcessor({ timeout: 5000, retries: 3 }); + await drain( + p.streamChunks( + [ + { + chunkIndex: 0, + externalLink: "https://example.com/stale", + httpHeaders: { "x-key": "stale" }, + }, + ], + undefined, + refresh, + ), + ); + + expect(fetchMock.mock.calls.map(([, init]) => init?.headers)).toEqual([ + { "x-key": "stale" }, + { "x-key": "fresh" }, + ]); + }); + + test.each([400, 404, 410])( + "a %i is not retried, and the error carries the code and status", + async (status) => { + const fetchMock = vi.fn().mockResolvedValue(response(status, "No")); + globalThis.fetch = fetchMock; + const refresh = vi.fn(); + + const p = new ArrowStreamProcessor({ timeout: 5000, retries: 3 }); + const error = await failureOf( + p.streamChunks(mockChunks(1), undefined, refresh), + ); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(refresh).not.toHaveBeenCalled(); + expect(error.errorCode).toBe(EXTERNAL_LINK_DOWNLOAD_FAILED); + expect(error.context?.httpStatus).toBe(status); + }, + ); + + test("a 403 re-mints the link once, retries at once, then stops", async () => { + const fetchMock = vi.fn().mockResolvedValue(response(403, "Forbidden")); + globalThis.fetch = fetchMock; + const refresh = vi.fn(async (chunkIndex: number) => ({ + chunkIndex, + externalLink: "https://example.com/fresh", + })); + + // retries: 1 proves the re-mint retry is not charged to the attempt budget. + const p = new ArrowStreamProcessor({ timeout: 5000, retries: 1 }); + const started = Date.now(); + const error = await failureOf( + p.streamChunks(mockChunks(1), undefined, refresh), + ); + + expect(Date.now() - started).toBeLessThan(500); + expect(refresh).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(error.errorCode).toBe(EXTERNAL_LINK_DOWNLOAD_FAILED); + expect(error.context?.httpStatus).toBe(403); + }); + + test.each([429, 408])( + "a %i is retried after the backoff and can succeed", + async (status) => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce(response(status)) + .mockResolvedValueOnce(okResponse(5)); + globalThis.fetch = fetchMock; + + const p = new ArrowStreamProcessor({ timeout: 5000, retries: 3 }); + const bytes = await drain(p.streamChunks(mockChunks(1))); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(bytes).toEqual([5]); + }, + 10000, + ); + + test("a 5xx keeps retrying and ends without a client-error status", async () => { + const fetchMock = vi.fn().mockResolvedValue(response(503, "Unavailable")); + globalThis.fetch = fetchMock; + + const p = new ArrowStreamProcessor({ timeout: 5000, retries: 2 }); + const error = await failureOf(p.streamChunks(mockChunks(1))); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(error.errorCode).toBe(EXTERNAL_LINK_DOWNLOAD_FAILED); + expect(error.context?.httpStatus).toBe(503); + }, 10000); +}); + +describe("ArrowStreamProcessor — storage that stalls", () => { + let server: Server; + let gets = 0; + let handler: Parameters[1]; + + const link = () => + `http://127.0.0.1:${(server.address() as AddressInfo).port}/chunk`; + const chunks = () => [{ chunkIndex: 0, externalLink: link() }]; + + beforeEach(async () => { + gets = 0; + server = createServer((req, res) => { + gets++; + handler?.(req, res); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + }); + + afterEach(async () => { + server.closeAllConnections(); + await new Promise((r) => server.close(r)); + }); + + test("no response headers within responseTimeoutMs is a denial and is not retried", async () => { + handler = () => {}; + + const p = new ArrowStreamProcessor({ + timeout: 5000, + retries: 3, + responseTimeoutMs: 100, + }); + const started = Date.now(); + const error = await failureOf(p.streamChunks(chunks())); + + expect(gets).toBe(1); + expect(Date.now() - started).toBeLessThan(1000); + expect(error.errorCode).toBe(EXTERNAL_LINK_DOWNLOAD_FAILED); + expect(error.context?.httpStatus).toBeUndefined(); + }); + + test("the default wait for response headers is 10 seconds", () => { + expect(ArrowStreamProcessor.DEFAULT_RESPONSE_TIMEOUT).toBe(10_000); + }); + + test("a healthy chunk that streams for longer than responseTimeoutMs finishes", async () => { + handler = (_req, res) => { + res.writeHead(200); + let sent = 0; + const timer = setInterval(() => { + res.write(Buffer.from([sent++])); + if (sent === 8) { + clearInterval(timer); + res.end(); + } + }, 60); + }; + + const p = new ArrowStreamProcessor({ + timeout: 1000, + retries: 1, + responseTimeoutMs: 100, + }); + const bytes = await drain(p.streamChunks(chunks())); + + expect(bytes).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); + }); + + test("a body that stalls after the headers fails with the download code", async () => { + handler = (_req, res) => { + res.writeHead(200, { "content-length": 1000 }); + res.flushHeaders(); + }; + + const p = new ArrowStreamProcessor({ + timeout: 150, + retries: 3, + responseTimeoutMs: 100, + }); + const error = await failureOf(p.streamChunks(chunks())); + + expect(gets).toBe(1); + expect(error.errorCode).toBe(EXTERNAL_LINK_DOWNLOAD_FAILED); + }); + + test("a connection reset after the headers fails with the download code", async () => { + handler = (req, res) => { + res.writeHead(200, { "content-length": 1000 }); + res.flushHeaders(); + setTimeout(() => req.socket.destroy(), 30); + }; + + const p = new ArrowStreamProcessor({ timeout: 5000, retries: 3 }); + const error = await failureOf(p.streamChunks(chunks())); + + expect(error.errorCode).toBe(EXTERNAL_LINK_DOWNLOAD_FAILED); + }); + + test("patient mode waits for headers as long as the attempt timeout", async () => { + handler = (_req, res) => { + setTimeout(() => res.end(Buffer.from([9])), 300); + }; + const p = new ArrowStreamProcessor({ + timeout: 2000, + retries: 1, + responseTimeoutMs: 100, + }); + + const strict = await failureOf(p.streamChunks(chunks())); + expect(strict.errorCode).toBe(EXTERNAL_LINK_DOWNLOAD_FAILED); + + const bytes = await drain( + p.streamChunks(chunks(), undefined, undefined, true), + ); + expect(bytes).toEqual([9]); + }); + + test("an abort from the caller still reads as a cancellation", async () => { + handler = () => {}; + const controller = new AbortController(); + const p = new ArrowStreamProcessor({ + timeout: 5000, + retries: 3, + responseTimeoutMs: 5000, + }); + const failed = failureOf(p.streamChunks(chunks(), controller.signal)); + setTimeout(() => controller.abort(), 50); + + const error = await failed; + expect(error.errorCode).toBeUndefined(); + expect(error.message).toMatch(/canceled/); + }); +}); From e93840c2e129533e1e09cc96a5c955f2f5cd101f Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 9 Nov 2026 16:25:46 +0000 Subject: [PATCH 2/9] fix(appkit): fall back to inline results when a Cloud Fetch download is refused A chart on the Arrow path asks for ARROW_STREAM, which a standard SQL warehouse serves only as EXTERNAL_LINKS. When workspace storage refused the app server's download, the route made three attempts and returned HTTP 500 with "Query execution failed". When storage never answered, the route's 120 s first-byte timer returned a 503 that blamed the warehouse (LA-2169). Processor - Send the link's httpHeaders with every GET, and the new link's headers after a re-mint. - Wait at most responseTimeoutMs (10 s) for response headers only. A timeout is not retried. Body reads keep the per-read idle timeout, so a healthy chunk may stream for longer. - A 403 re-mints the link once and retries at once. Any other 4xx except 408 and 429 stops. Network errors and 5xx keep the 1 s and 2 s backoff. - Every download failure, before or after the headers, carries errorCode EXTERNAL_LINK_DOWNLOAD_FAILED and the last HTTP status. Delivery - deliverArrowBytesWithFallback pulls the first chunk before the route sends headers. On a failure before the first byte it runs the query as INLINE + JSON_ARRAY and rebuilds the Arrow IPC stream from the positional rows and manifest columns, typed by parseDatabricksType. - Only a 400 or 403 after the re-mint is remembered as "links-denied", for ten minutes. A timeout, network error, or body failure falls back once and is forgotten. - A result the inline run cannot hold (manifest byte count over 25 MiB, or a multi-chunk inline response) is never served truncated. After an inconclusive failure Cloud Fetch gets one patient pass. After a refusal the error is EXTERNAL_LINKS_DENIED and says why. - Any other inline failure is rethrown unchanged. A rebuild failure is ARROW_REBUILD_FAILED. Connector - executeStatement accepts rawRows to keep dataArray and carry the manifest columns on the result. EXTERNAL_LINKS results now carry totalByteCount. The inline fallback run is cached like the inline Arrow attempt, under its own key. @databricks/appkit-ui is unchanged. Signed-off-by: Pedro Figueiredo --- .../connectors/sql-warehouse/arrow-schema.ts | 181 ++++++++++++++ .../src/connectors/sql-warehouse/client.ts | 37 ++- .../appkit/src/plugins/analytics/analytics.ts | 69 ++++-- .../src/plugins/analytics/result-delivery.ts | 225 +++++++++++++++++- .../src/stream/arrow-stream-processor.ts | 215 +++++++++++------ 5 files changed, 630 insertions(+), 97 deletions(-) diff --git a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts index af5bfbb18..8193a074e 100644 --- a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts +++ b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts @@ -21,7 +21,12 @@ import { Table, TimestampMicrosecond, tableToIPC, + Type, Utf8, + type Vector, + makeData, + makeVector, + vectorFromArray, } from "apache-arrow"; /** @@ -75,6 +80,182 @@ export function buildEmptyArrowIPCBase64( return Buffer.from(ipc).toString("base64"); } +/** Column metadata from `manifest.schema.columns`. */ +export interface ArrowRowColumn { + name?: string; + typeText?: string; + typeName?: string; + typePrecision?: number; + typeScale?: number; +} + +/** + * Rebuild the Arrow IPC stream a warehouse serves for `ARROW_STREAM` from the + * positional string rows of an `INLINE` + `JSON_ARRAY` result. Fields are named + * `col_N` like the native stream, and each type is what + * {@link parseDatabricksType} gives that column, with DECIMAL precision and + * scale taken from `typePrecision` and `typeScale`. + * + * Nested, interval, and geospatial columns arrive as strings on the JSON wire + * and stay strings. A cell that does not parse as its column type throws. + */ +export function buildArrowIPCFromRows( + columns: ArrowRowColumn[], + rows: Array>, +): Uint8Array { + const vectors: Record = {}; + columns.forEach((col, index) => { + const cells = rows.map((row) => row[index] ?? null); + vectors[`col_${index}`] = vectorFromCells(col, cells); + }); + return tableToIPC(new Table(vectors), "stream"); +} + +function vectorFromCells(col: ArrowRowColumn, cells: Array) { + const type = rebuildType(col); + const map = (parse: (cell: string) => T) => + cells.map((cell) => (cell === null ? null : parse(cell))); + switch (type.typeId) { + case Type.Int: { + const { bitWidth } = type as unknown as { bitWidth: number }; + return vectorFromArray( + bitWidth === 64 ? map(parseBigInt) : map(parseInteger), + type as never, + ); + } + case Type.Float: + return vectorFromArray(map(parseFloatCell), type as never); + case Type.Bool: + return vectorFromArray(map(parseBoolean), type as never); + case Type.Date: + return vectorFromArray(map(parseUtcMillis), type as never); + case Type.Timestamp: + return vectorFromArray(map(parseUtcMillis), type as never); + case Type.Binary: + return vectorFromArray( + map((cell) => new Uint8Array(Buffer.from(cell, "base64"))), + type as never, + ); + case Type.Decimal: { + const { scale, precision } = type as unknown as { + scale: number; + precision: number; + }; + return decimalVector(cells, scale, precision); + } + case Type.Null: + return vectorFromArray( + cells.map(() => null), + type as never, + ); + default: + return vectorFromArray(cells, new Utf8()); + } +} + +const STRING_ONLY_TYPES = new Set([ + Type.List, + Type.Struct, + Type.Map, + Type.Interval, + Type.Duration, +]); + +function rebuildType(col: ArrowRowColumn): DataType { + const typeText = col.typeText ?? col.typeName ?? "STRING"; + let type: DataType; + try { + type = parseDatabricksType(typeText); + } catch { + return new Utf8(); + } + if (STRING_ONLY_TYPES.has(type.typeId)) return new Utf8(); + // GEOGRAPHY and GEOMETRY map to Binary but arrive as text on the JSON wire. + if (type.typeId === Type.Binary && typeText.toUpperCase() !== "BINARY") { + return new Utf8(); + } + if (type.typeId === Type.Decimal && col.typePrecision != null) { + return new Decimal(col.typeScale ?? 0, col.typePrecision, 128); + } + return type; +} + +function parseInteger(cell: string): number { + const n = Number(cell); + if (cell.trim() === "" || !Number.isInteger(n)) { + throw new Error(`Invalid integer "${cell}"`); + } + return n; +} + +function parseBigInt(cell: string): bigint { + if (cell.trim() === "") throw new Error(`Invalid integer "${cell}"`); + return BigInt(cell); +} + +function parseFloatCell(cell: string): number { + const n = Number(cell); + if (cell.trim() === "" || (Number.isNaN(n) && cell !== "NaN")) { + throw new Error(`Invalid number "${cell}"`); + } + return n; +} + +function parseBoolean(cell: string): boolean { + if (cell !== "true" && cell !== "false") { + throw new Error(`Invalid boolean "${cell}"`); + } + return cell === "true"; +} + +/** Epoch ms for an ISO date or timestamp; a value with no zone is UTC. */ +function parseUtcMillis(cell: string): number { + const ms = Date.parse( + /(?:Z|[+-]\d\d(?::?\d\d)?)$/i.test(cell) || !cell.includes("T") + ? cell + : `${cell}Z`, + ); + if (Number.isNaN(ms)) throw new Error(`Invalid date or timestamp "${cell}"`); + return ms; +} + +function decimalVector( + cells: Array, + scale: number, + precision: number, +) { + const words = new Uint32Array(cells.length * 4); + const validity = new Uint8Array(Math.ceil(cells.length / 8)); + let nullCount = 0; + cells.forEach((cell, i) => { + if (cell === null) { + nullCount++; + return; + } + validity[i >> 3] |= 1 << (i & 7); + const negative = cell.startsWith("-"); + const [whole, fraction = ""] = cell.replace(/^[-+]/, "").split("."); + const digits = `${whole}${fraction.padEnd(scale, "0").slice(0, scale)}`; + if (!/^\d+$/.test(digits)) throw new Error(`Invalid decimal "${cell}"`); + const unscaled = BigInt(digits); + const twosComplement = negative ? (1n << 128n) - unscaled : unscaled; + for (let w = 0; w < 4; w++) { + words[i * 4 + w] = Number( + (twosComplement >> BigInt(32 * w)) & 0xffffffffn, + ); + } + }); + return makeVector( + makeData({ + type: new Decimal(scale, precision, 128), + length: cells.length, + nullCount, + nullBitmap: validity, + data: words, + }), + ); +} + // ============================================================================ // Recursive-descent parser // ============================================================================ diff --git a/packages/appkit/src/connectors/sql-warehouse/client.ts b/packages/appkit/src/connectors/sql-warehouse/client.ts index b980a66d8..73d6128a9 100644 --- a/packages/appkit/src/connectors/sql-warehouse/client.ts +++ b/packages/appkit/src/connectors/sql-warehouse/client.ts @@ -102,7 +102,7 @@ function normalizeStatementCounts(response: T): T { * results fall through to `disposition: "EXTERNAL_LINKS"`, which streams * chunk-by-chunk and is not bound by this limit. */ -const MAX_INLINE_ATTACHMENT_BYTES = 25 * 1024 * 1024; +export const MAX_INLINE_ATTACHMENT_BYTES = 25 * 1024 * 1024; /** * Safety cap on how many additional EXTERNAL_LINKS chunks @@ -246,7 +246,13 @@ export class SQLWarehouseConnector { async executeStatement( workspaceClient: WorkspaceClient, - input: ExecuteStatementRequest, + input: ExecuteStatementRequest & { + /** + * Keep the warehouse's positional `dataArray` and carry the manifest + * `columns` on the result, instead of building keyed row objects. + */ + rawRows?: boolean; + }, signal?: AbortSignal, ) { const startTime = Date.now(); @@ -353,6 +359,7 @@ export class SQLWarehouseConnector { statementId, this.config.timeout, signal, + input.rawRows, ); break; case "SUCCEEDED": @@ -360,6 +367,7 @@ export class SQLWarehouseConnector { response, workspaceClient, signal, + input.rawRows, ); break; case "FAILED": @@ -830,6 +838,7 @@ export class SQLWarehouseConnector { statementId: string, timeout = executeStatementDefaults.timeout, signal?: AbortSignal, + rawRows?: boolean, ) { return this.telemetry.startActiveSpan( "sql.poll", @@ -908,6 +917,7 @@ export class SQLWarehouseConnector { response, workspaceClient, signal, + rawRows, ); case "FAILED": throw ExecutionError.statementFailed( @@ -949,6 +959,7 @@ export class SQLWarehouseConnector { response: StatementResponse, workspaceClient: WorkspaceClient, signal?: AbortSignal, + rawRows?: boolean, ) { if (response.manifest?.format === "ARROW_STREAM") { const result = response.result as @@ -998,11 +1009,22 @@ export class SQLWarehouseConnector { // current Databricks warehouses. } - if (!response.result?.dataArray || !response.manifest?.schema?.columns) { - return response; + const columns = response.manifest?.schema?.columns; + if (rawRows && columns) { + return { + ...response, + result: { + ...response.result, + columns, + columnNames: arrowColumnNames(response), + statement_id: response.statementId, + }, + }; } - const columns = response.manifest.schema.columns; + if (!response.result?.dataArray || !columns) { + return response; + } const transformedData = response.result.dataArray.map((row) => { const obj: Record = {}; @@ -1108,6 +1130,7 @@ export class SQLWarehouseConnector { statement_id: string; status: StatementStatus; columnNames?: string[]; + totalByteCount?: number; external_links?: ExternalLink[]; refreshChunkLink?: RefreshChunkLink; }; @@ -1121,6 +1144,7 @@ export class SQLWarehouseConnector { error: response.status?.error, } as StatementStatus, columnNames: arrowColumnNames(response), + totalByteCount: response.manifest?.totalByteCount as number | undefined, // Resolve the pre-signed links for EVERY chunk in the caller's own // execution context. Streaming these directly (see // {@link streamExternalLinks}) avoids a second `getStatement` under @@ -1227,8 +1251,9 @@ export class SQLWarehouseConnector { chunks: ExternalLink[], signal?: AbortSignal, refresh?: RefreshChunkLink, + patient?: boolean, ): AsyncGenerator { - return this.arrowProcessor.streamChunks(chunks, signal, refresh); + return this.arrowProcessor.streamChunks(chunks, signal, refresh, patient); } /** diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 1d18aae6d..7834114f2 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -48,7 +48,8 @@ import { import { QueryProcessor } from "./query"; import { type ArrowCapability, - deliverArrowBytes, + type ArrowDeliveryOut, + deliverArrowBytesWithFallback, deliverJsonResult, type QueryExecutor, } from "./result-delivery"; @@ -133,6 +134,9 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { */ private _arrowCapability = new Map(); + /** When each warehouse's `"links-denied"` entry lapses back to `"external"`. */ + private _linksDeniedUntil = new Map(); + constructor(config: IAnalyticsConfig) { super(config); this.config = config; @@ -861,10 +865,11 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { const warehouseId = await getWarehouseId(); - // Populated by `deliverArrowBytes` from the result manifest before the - // first chunk is yielded, so the header below carries the real names. - const columnsRef: { columnNames?: string[]; statementId?: string } = {}; - const bytes = deliverArrowBytes( + // Populated by `deliverArrowBytesWithFallback` from the result manifest + // before the first chunk is yielded, so the header below carries the + // real names. + const columnsRef: ArrowDeliveryOut = {}; + const bytes = deliverArrowBytesWithFallback( // Wrap the executor so the INLINE attempt runs through the interceptor // chain (cache + retry), matching the JSON path. Only inline attachments // are cached; EXTERNAL_LINKS carry expiring pre-signed URLs and are @@ -884,9 +889,16 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { { // Skip the doomed INLINE probe on a warehouse already known to need // EXTERNAL_LINKS; remember the resolved mode for next time. - capabilityHint: this._arrowCapability.get(warehouseId), - onCapabilityResolved: (capability) => - this._arrowCapability.set(warehouseId, capability), + capabilityHint: this._arrowCapabilityHint(warehouseId), + onCapabilityResolved: (capability) => { + this._arrowCapability.set(warehouseId, capability); + if (capability === "links-denied") { + this._linksDeniedUntil.set( + warehouseId, + Date.now() + LINKS_DENIED_TTL_MS, + ); + } + }, }, ); const first = await bytes.next(); @@ -964,6 +976,24 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { } } + /** + * The remembered arrow capability for a warehouse. A lapsed `"links-denied"` + * reverts to `"external"`, so the next query tries Cloud Fetch again. + */ + private _arrowCapabilityHint( + warehouseId: string, + ): ArrowCapability | undefined { + const capability = this._arrowCapability.get(warehouseId); + if ( + capability === "links-denied" && + Date.now() >= (this._linksDeniedUntil.get(warehouseId) ?? 0) + ) { + this._arrowCapability.set(warehouseId, "external"); + return "external"; + } + return capability; + } + /** * Await SQL warehouse readiness for the direct-binary Arrow path. There is no * SSE progress channel here — readiness is simply awaited, bounded by the @@ -994,6 +1024,8 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { * * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the * same lifecycle as cached JSON rows — safe to cache. + * - The `INLINE + JSON_ARRAY` run behind the Cloud Fetch fallback (the + * `rawRows` request) is bounded the same way, and cached under its own key. * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in * minutes; caching them would serve dead links, so those pass through * uncached (only the tiny link metadata would be cached anyway). @@ -1016,12 +1048,16 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { const ttl = queryDefaults.cache?.ttl; return { query: (q, params, formatParameters, signal) => { - // Only the inline-arrow attempt is cacheable — EXTERNAL_LINKS carry + // Only inline attempts are cacheable — EXTERNAL_LINKS carry // short-lived pre-signed URLs, so those pass straight through. - if ( - formatParameters.disposition !== "INLINE" || - formatParameters.format !== "ARROW_STREAM" - ) { + const inlineArrow = + formatParameters.disposition === "INLINE" && + formatParameters.format === "ARROW_STREAM"; + const inlineRows = + formatParameters.disposition === "INLINE" && + formatParameters.format === "JSON_ARRAY" && + formatParameters.rawRows; + if (!inlineArrow && !inlineRows) { return executor.query(q, params, formatParameters, signal); } // On a standard warehouse this throws a capability rejection — the @@ -1031,7 +1067,9 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { // dedupes concurrent renders (e.g. React StrictMode double-mount). return cache.getOrExecute( [ - "analytics:query:arrow", + inlineArrow + ? "analytics:query:arrow" + : "analytics:query:arrow-rows", query_key, JSON.stringify(parameters), hashedQuery, @@ -1208,6 +1246,9 @@ export function writeChunk( */ const DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000; +/** How long a definite Cloud Fetch refusal is remembered before storage is tried again. */ +const LINKS_DENIED_TTL_MS = 10 * 60_000; + /** * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a * very wide schema) the names are served via the `/columns/:statementId` diff --git a/packages/appkit/src/plugins/analytics/result-delivery.ts b/packages/appkit/src/plugins/analytics/result-delivery.ts index 3f40439d2..5803a9c0b 100644 --- a/packages/appkit/src/plugins/analytics/result-delivery.ts +++ b/packages/appkit/src/plugins/analytics/result-delivery.ts @@ -1,9 +1,17 @@ import { type DataType, type Field, Type, tableFromIPC } from "apache-arrow"; import type { SQLTypeMarker } from "shared"; +import { + type ArrowRowColumn, + buildArrowIPCFromRows, +} from "../../connectors/sql-warehouse/arrow-schema"; +import { MAX_INLINE_ATTACHMENT_BYTES } from "../../connectors/sql-warehouse/client"; import { ExecutionError } from "../../errors"; import { createLogger } from "../../logging/logger"; -import type { RefreshChunkLink } from "../../stream/arrow-stream-processor"; +import { + EXTERNAL_LINK_DOWNLOAD_FAILED, + type RefreshChunkLink, +} from "../../stream/arrow-stream-processor"; import type { ExternalLink } from "../../workspace-client"; /** @@ -33,14 +41,24 @@ export interface QueryExecutor { query( query: string, parameters: Record | undefined, - formatParameters: { disposition: string; format: string }, + formatParameters: { + disposition: string; + format: string; + /** Ask for positional `dataArray` rows plus manifest `columns`. */ + rawRows?: boolean; + }, signal?: AbortSignal, ): Promise< | { attachment?: string; data?: Record[]; + dataArray?: Array>; + columns?: ArrowRowColumn[]; external_links?: ExternalLink[]; columnNames?: string[]; + totalByteCount?: number; + nextChunkIndex?: number; + nextChunkInternalLink?: string; statement_id?: string; status?: unknown; refreshChunkLink?: RefreshChunkLink; @@ -55,11 +73,27 @@ export interface ArrowChunkStreamer { chunks: ExternalLink[], signal?: AbortSignal, refresh?: RefreshChunkLink, + patient?: boolean, ): AsyncGenerator; } -/** The arrow delivery mode a warehouse actually supports. */ -export type ArrowCapability = "inline" | "external"; +/** + * The arrow delivery mode a warehouse actually supports. `"links-denied"` means + * storage definitively refused the Cloud Fetch download, so results are served + * from an inline `JSON_ARRAY` run until the caller lets the hint lapse. + */ +export type ArrowCapability = "inline" | "external" | "links-denied"; + +/** Filled in by delivery for the route: column names and, for the fallback, how to re-stream. */ +export interface ArrowDeliveryOut { + columnNames?: string[]; + statementId?: string; + /** Set once EXTERNAL_LINKS resolves. */ + cloudFetch?: { + totalByteCount?: number; + stream(patient: boolean): AsyncGenerator; + }; +} /** Optional per-warehouse capability memo, avoiding a doomed probe per query. */ interface ArrowDeliveryOptions { @@ -190,7 +224,7 @@ export async function* deliverArrowBytes( streamer: ArrowChunkStreamer, query: string, processedParams: Record | undefined, - out: { columnNames?: string[]; statementId?: string }, + out: ArrowDeliveryOut, signal?: AbortSignal, opts?: ArrowDeliveryOptions, ): AsyncGenerator { @@ -271,11 +305,186 @@ export async function* deliverArrowBytes( // need no auth to download, so there is no second `getStatement` under a // mismatched identity. `refreshChunkLink` (also bound to this context) lets // the streamer re-mint a link that expires mid-download. - yield* streamer.streamExternalLinks( - ext.external_links, + const links = ext.external_links; + const stream = (patient: boolean) => + streamer.streamExternalLinks(links, signal, ext.refreshChunkLink, patient); + out.cloudFetch = { totalByteCount: ext.totalByteCount, stream }; + yield* stream(false); +} + +/** The inline result spans several chunks, and only the first is in hand. */ +class InlineDoesNotFit extends Error {} + +type InlineAttempt = { bytes: Uint8Array } | { error: unknown }; + +/** + * Run the query as `INLINE` + `JSON_ARRAY` and rebuild the Arrow IPC stream a + * Cloud Fetch download would have carried. The route sees the same bytes and + * header names either way. + */ +async function inlineJsonAsArrow( + executor: QueryExecutor, + query: string, + processedParams: Record | undefined, + out: ArrowDeliveryOut, + signal?: AbortSignal, +): Promise { + try { + const result = await executor.query( + query, + processedParams, + { disposition: "INLINE", format: "JSON_ARRAY", rawRows: true }, + signal, + ); + if ( + result?.nextChunkIndex != null || + result?.nextChunkInternalLink != null + ) { + return { error: new InlineDoesNotFit() }; + } + if (!result?.columns) throw ExecutionError.missingData("manifest columns"); + let bytes: Uint8Array; + try { + bytes = buildArrowIPCFromRows(result.columns, result.dataArray ?? []); + } catch (err: unknown) { + throw ExecutionError.statementFailed( + `Failed to rebuild Arrow from inline JSON_ARRAY rows: ${errMessage(err)}`, + "ARROW_REBUILD_FAILED", + 'The query result could not be converted to Arrow. Re-run the query with format="JSON_ARRAY".', + ); + } + out.columnNames = result.columnNames; + out.statementId = result.statement_id; + return { bytes }; + } catch (error: unknown) { + if (signal?.aborted) throw error; + return { error }; + } +} + +function isDownloadFailure(err: unknown): err is ExecutionError { + return ( + err instanceof ExecutionError && + err.errorCode === EXTERNAL_LINK_DOWNLOAD_FAILED + ); +} + +/** Download refused (or unanswered) and the result cannot be sent inline. */ +function linksDenied(cause: ExecutionError): ExecutionError { + return new ExecutionError( + `Cloud Fetch download failed and the result is too large to send inline: ${cause.message}`, + { + errorCode: "EXTERNAL_LINKS_DENIED", + cause, + clientMessage: + "The query result could not be downloaded from workspace storage and is too large to send inline. Check the storage account's firewall and the app's network policy.", + }, + ); +} + +/** + * {@link deliverArrowBytes} with a way out when the Cloud Fetch download fails + * before its first byte (storage refuses, resets, or never answers). + * + * The first chunk is pulled here, before the route sends headers, so a failed + * download can still be replaced by `inlineJsonAsArrow`. Only a 400 or 403 that + * survived the link re-mint counts as a definite refusal and reports + * `"links-denied"`; a timeout, network error, or body failure falls back once + * and is forgotten. A result too large for inline is never stranded: after an + * inconclusive failure Cloud Fetch gets one more, patient pass, and after a + * definite refusal the error says why. Any other inline failure is rethrown + * unchanged. + * + * With the `"links-denied"` hint the inline run goes first, and Cloud Fetch is + * tried only if that cannot deliver. + */ +export async function* deliverArrowBytesWithFallback( + executor: QueryExecutor, + streamer: ArrowChunkStreamer, + query: string, + processedParams: Record | undefined, + out: ArrowDeliveryOut, + signal?: AbortSignal, + opts?: ArrowDeliveryOptions, +): AsyncGenerator { + const hinted = opts?.capabilityHint === "links-denied"; + let tooLarge = false; + let inlineError: unknown; + if (hinted) { + const inline = await inlineJsonAsArrow( + executor, + query, + processedParams, + out, + signal, + ); + if ("bytes" in inline) { + yield inline.bytes; + return; + } + if (inline.error instanceof InlineDoesNotFit) tooLarge = true; + else inlineError = inline.error; + } + + const bytes = deliverArrowBytes( + executor, + streamer, + query, + processedParams, + out, signal, - ext.refreshChunkLink, + hinted ? { capabilityHint: "external" } : opts, ); + let first: IteratorResult; + let downloadError: ExecutionError | undefined; + try { + first = await bytes.next(); + } catch (err: unknown) { + if (signal?.aborted || !isDownloadFailure(err)) throw err; + downloadError = err; + first = { done: true, value: undefined }; + } + if (!downloadError) { + if (hinted) opts?.onCapabilityResolved?.("external"); + if (!first.done) yield first.value; + yield* bytes; + return; + } + + const status = downloadError.context?.httpStatus; + const refused = status === 400 || status === 403; + if (refused) opts?.onCapabilityResolved?.("links-denied"); + const size = out.cloudFetch?.totalByteCount; + tooLarge ||= size !== undefined && size > MAX_INLINE_ATTACHMENT_BYTES; + if (!tooLarge && inlineError === undefined) { + const inline = await inlineJsonAsArrow( + executor, + query, + processedParams, + out, + signal, + ); + if ("bytes" in inline) { + yield inline.bytes; + return; + } + if (!(inline.error instanceof InlineDoesNotFit)) throw inline.error; + tooLarge = true; + } + if (!tooLarge) throw inlineError; + if (refused || !out.cloudFetch) throw linksDenied(downloadError); + + const patient = out.cloudFetch.stream(true); + let retried: IteratorResult; + try { + retried = await patient.next(); + } catch (err: unknown) { + if (signal?.aborted || !isDownloadFailure(err)) throw err; + throw linksDenied(err); + } + if (hinted) opts?.onCapabilityResolved?.("external"); + if (!retried.done) yield retried.value; + yield* patient; } /** JSON row result plus the metadata the SSE `result` message forwards. */ diff --git a/packages/appkit/src/stream/arrow-stream-processor.ts b/packages/appkit/src/stream/arrow-stream-processor.ts index e063b6abb..822de4a88 100644 --- a/packages/appkit/src/stream/arrow-stream-processor.ts +++ b/packages/appkit/src/stream/arrow-stream-processor.ts @@ -18,14 +18,40 @@ export type RefreshChunkLink = ( ) => Promise; interface ArrowStreamOptions { - /** Idle timeout (ms) for a single chunk: no progress → abort the download. */ + /** Idle timeout (ms) for a single read, and the response wait in patient mode. */ timeout: number; /** Attempts to establish a chunk's response before any bytes are yielded. */ retries: number; + /** Longest wait (ms) for response headers before the download counts as denied. */ + responseTimeoutMs: number; } +/** `errorCode` on every failure to download a chunk, whether before or after the response headers. */ +export const EXTERNAL_LINK_DOWNLOAD_FAILED = "EXTERNAL_LINK_DOWNLOAD_FAILED"; + const BACKOFF_MULTIPLIER = 1000; +type Connection = + | { + ok: true; + response: Response; + controller: AbortController; + detach: () => void; + } + | { ok: false; status?: number; error: unknown; timedOut: boolean }; + +function downloadFailed( + message: string, + httpStatus?: number, + cause?: unknown, +): ExecutionError { + return new ExecutionError(message, { + errorCode: EXTERNAL_LINK_DOWNLOAD_FAILED, + context: { httpStatus }, + cause: cause instanceof Error ? cause : undefined, + }); +} + /** * Streams Arrow IPC bytes for a completed statement's EXTERNAL_LINKS chunks * without ever buffering a whole chunk — each chunk's response body is piped @@ -36,6 +62,7 @@ const BACKOFF_MULTIPLIER = 1000; export class ArrowStreamProcessor { static readonly DEFAULT_TIMEOUT = 30000; static readonly DEFAULT_RETRIES = 3; + static readonly DEFAULT_RESPONSE_TIMEOUT = 10000; private options: ArrowStreamOptions; @@ -43,6 +70,9 @@ export class ArrowStreamProcessor { this.options = { timeout: options?.timeout ?? ArrowStreamProcessor.DEFAULT_TIMEOUT, retries: options?.retries ?? ArrowStreamProcessor.DEFAULT_RETRIES, + responseTimeoutMs: + options?.responseTimeoutMs ?? + ArrowStreamProcessor.DEFAULT_RESPONSE_TIMEOUT, }; } @@ -53,18 +83,23 @@ export class ArrowStreamProcessor { * memory is one read buffer. Chunks are fetched sequentially; the * concatenation of everything yielded is byte-identical to the raw Arrow * result, so the client parses it exactly as a buffered response. + * + * `patient` waits for response headers as long as the per-attempt `timeout` + * instead of `responseTimeoutMs`, for a second pass over a download that + * only looked stalled. */ async *streamChunks( chunks: ExternalLink[], signal?: AbortSignal, refresh?: RefreshChunkLink, + patient = false, ): AsyncGenerator { if (chunks.length === 0) { throw ValidationError.missingField("chunks"); } for (const chunk of chunks) { - yield* this.streamChunkBody(chunk, signal, refresh); + yield* this.streamChunkBody(chunk, signal, refresh, patient); } } @@ -75,89 +110,92 @@ export class ArrowStreamProcessor { * or non-2xx) — once bytes have been yielded downstream we cannot re-fetch, * so a mid-body failure propagates and the caller aborts the response. An * idle timeout, reset before every read, aborts a stalled download. + * + * A 403 re-mints the link once and retries at once, since a pre-signed URL + * may have expired. Any other 4xx except 408 and 429 stops, and so does a + * wait for headers that outlasts the limit: a blocked host can hang for + * minutes instead of refusing, and a retry would only repeat the wait. */ private async *streamChunkBody( chunk: ExternalLink, - signal?: AbortSignal, - refresh?: RefreshChunkLink, + signal: AbortSignal | undefined, + refresh: RefreshChunkLink | undefined, + patient: boolean, ): AsyncGenerator { - let externalLink = chunk.externalLink; - if (!externalLink) { + let link = chunk; + if (!link.externalLink) { // A missing link cannot be fixed by retrying — fail loudly. throw ExecutionError.statementFailed( `External link missing for chunk ${chunk.chunkIndex}`, ); } - let response: Response | undefined; - let controller: AbortController | undefined; - let onOuterAbort: (() => void) | undefined; - let lastError: unknown; + const { timeout, retries, responseTimeoutMs } = this.options; + const headerWaitMs = patient + ? timeout + : Math.min(responseTimeoutMs, timeout); + let connection: Extract | undefined; + let failure: Extract | undefined; + let attempts = 0; + let reminted = false; - for (let attempt = 0; attempt < this.options.retries; attempt++) { + while (attempts < retries) { if (signal?.aborted) throw ExecutionError.canceled(); + attempts++; + const result = await this.connect(link, headerWaitMs, signal); + if (result.ok) { + connection = result; + break; + } + failure = result; + if (signal?.aborted) throw ExecutionError.canceled(); + if (result.timedOut) break; - const attemptController = new AbortController(); - const listener = () => attemptController.abort(); - signal?.addEventListener("abort", listener, { once: true }); - const timer = setTimeout( - () => attemptController.abort(), - this.options.timeout, - ); - - try { - const r = await fetch(externalLink, { - signal: attemptController.signal, - }); - clearTimeout(timer); - if (!r.ok) { - throw ExecutionError.statementFailed( - `Failed to download chunk ${chunk.chunkIndex}: ${r.status} ${r.statusText}`, + const { status } = result; + if (status === 403 && refresh && chunk.chunkIndex != null && !reminted) { + reminted = true; + attempts--; + try { + const fresh = await refresh(chunk.chunkIndex, signal); + if (fresh?.externalLink) link = fresh; + } catch (refreshError) { + // Retry the current URL; surface the original error if it fails. + logger.warn( + "Failed to re-resolve link for chunk %s: %O", + chunk.chunkIndex, + refreshError, ); } - // Keep this attempt's controller alive to drive the body read + idle - // timeout below. - response = r; - controller = attemptController; - onOuterAbort = listener; + continue; + } + if ( + status !== undefined && + status >= 400 && + status < 500 && + status !== 408 && + status !== 429 + ) { break; - } catch (error) { - clearTimeout(timer); - signal?.removeEventListener("abort", listener); - lastError = error; - if (signal?.aborted) throw ExecutionError.canceled(); - if (attempt < this.options.retries - 1) { - await this.delay(2 ** attempt * BACKOFF_MULTIPLIER, signal); - // Pre-signed URLs expire (<= 15 min). Before retrying, re-mint this - // chunk's link — a stale URL would just 403 again on the same address. - // Only meaningful before any bytes are yielded (below), which is why - // this lives in the establish-response loop. - if (refresh && chunk.chunkIndex != null) { - try { - const fresh = await refresh(chunk.chunkIndex, signal); - if (fresh?.externalLink) externalLink = fresh.externalLink; - } catch (refreshError) { - // Keep retrying the current URL; surface the original error if - // all attempts fail. - logger.warn( - "Failed to re-resolve link for chunk %s: %O", - chunk.chunkIndex, - refreshError, - ); - } - } - } + } + if (attempts < retries) { + await this.delay(2 ** (attempts - 1) * BACKOFF_MULTIPLIER, signal); } } - if (!response || !controller) { - throw ExecutionError.statementFailed( - `Failed to download chunk ${chunk.chunkIndex} after ${this.options.retries} attempts: ${ - lastError instanceof Error ? lastError.message : String(lastError) - }`, + if (!connection) { + const reason = failure?.timedOut + ? `no response within ${headerWaitMs}ms` + : failure?.error instanceof Error + ? failure.error.message + : String(failure?.error); + throw downloadFailed( + `Failed to download chunk ${chunk.chunkIndex}: ${reason}`, + failure?.status, + failure?.error, ); } + const { response, controller, detach } = connection; const body = response.body; let idleTimer: ReturnType | undefined; try { @@ -170,10 +208,7 @@ export class ArrowStreamProcessor { // it the instant the read resolves — if it stayed armed across the // yield, a slow client backpressuring `writeChunk` would look like an // upstream stall and abort a perfectly healthy download. - idleTimer = setTimeout( - () => controller.abort(), - this.options.timeout, - ); + idleTimer = setTimeout(() => controller.abort(), timeout); let done: boolean; let value: Uint8Array | undefined; try { @@ -197,14 +232,56 @@ export class ArrowStreamProcessor { ); throw error instanceof ExecutionError ? error - : ExecutionError.statementFailed( + : downloadFailed( `Failed streaming chunk ${chunk.chunkIndex}: ${ error instanceof Error ? error.message : String(error) }`, + undefined, + error, ); } finally { clearTimeout(idleTimer); - if (onOuterAbort) signal?.removeEventListener("abort", onOuterAbort); + detach(); + } + } + + /** + * One GET. Only the wait for response headers is timed here; the timer is + * cleared the moment they arrive, so a healthy chunk may stream for longer. + */ + private async connect( + link: ExternalLink, + headerWaitMs: number, + signal?: AbortSignal, + ): Promise { + const controller = new AbortController(); + const onAbort = () => controller.abort(); + signal?.addEventListener("abort", onAbort, { once: true }); + const detach = () => signal?.removeEventListener("abort", onAbort); + const timer = setTimeout(() => controller.abort(), headerWaitMs); + try { + const response = await fetch(link.externalLink as string, { + signal: controller.signal, + headers: link.httpHeaders, + }); + clearTimeout(timer); + if (response.ok) return { ok: true, response, controller, detach }; + await response.body?.cancel().catch(() => {}); + detach(); + return { + ok: false, + status: response.status, + error: new Error(`${response.status} ${response.statusText}`), + timedOut: false, + }; + } catch (error) { + clearTimeout(timer); + detach(); + return { + ok: false, + error, + timedOut: !signal?.aborted && controller.signal.aborted, + }; } } From dbcad69482b2e8b70bd4c1932d068e4aa0e2484c Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 9 Nov 2026 17:51:51 +0000 Subject: [PATCH 3/9] fix(appkit): remember a Cloud Fetch download that never answers Builder pods run with internet off and no SEG, so a download straight to cloud storage hangs instead of refusing. Remember that as links-denied once the inline fallback has served the result, so later charts skip the wait. Signed-off-by: Pedro Figueiredo --- .../src/plugins/analytics/result-delivery.ts | 31 +++++++++++++------ .../cloud-fetch-fallback.integration.test.ts | 6 ++-- .../analytics/tests/result-delivery.test.ts | 14 +++++++-- .../src/stream/arrow-stream-processor.ts | 4 ++- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/result-delivery.ts b/packages/appkit/src/plugins/analytics/result-delivery.ts index 5803a9c0b..e955b6211 100644 --- a/packages/appkit/src/plugins/analytics/result-delivery.ts +++ b/packages/appkit/src/plugins/analytics/result-delivery.ts @@ -79,7 +79,7 @@ export interface ArrowChunkStreamer { /** * The arrow delivery mode a warehouse actually supports. `"links-denied"` means - * storage definitively refused the Cloud Fetch download, so results are served + * storage refused or never answered the Cloud Fetch download, so results are served * from an inline `JSON_ARRAY` run until the caller lets the hint lapse. */ export type ArrowCapability = "inline" | "external" | "links-denied"; @@ -369,6 +369,12 @@ function isDownloadFailure(err: unknown): err is ExecutionError { ); } +/** Storage answered 400 or 403 even after the link re-mint. */ +function isRefusal(err: ExecutionError): boolean { + const status = err.context?.httpStatus; + return status === 400 || status === 403; +} + /** Download refused (or unanswered) and the result cannot be sent inline. */ function linksDenied(cause: ExecutionError): ExecutionError { return new ExecutionError( @@ -387,13 +393,14 @@ function linksDenied(cause: ExecutionError): ExecutionError { * before its first byte (storage refuses, resets, or never answers). * * The first chunk is pulled here, before the route sends headers, so a failed - * download can still be replaced by `inlineJsonAsArrow`. Only a 400 or 403 that - * survived the link re-mint counts as a definite refusal and reports - * `"links-denied"`; a timeout, network error, or body failure falls back once - * and is forgotten. A result too large for inline is never stranded: after an - * inconclusive failure Cloud Fetch gets one more, patient pass, and after a - * definite refusal the error says why. Any other inline failure is rethrown - * unchanged. + * download can still be replaced by `inlineJsonAsArrow`. A 400 or 403 that + * survived the link re-mint reports `"links-denied"` at once. No response at all + * reports it once the inline result has been served, or once the patient pass + * also fails, because blocked hosts hang instead of refusing. A network error + * or body failure falls back once and is forgotten. A result too large for + * inline is never stranded: after an inconclusive failure Cloud Fetch gets one + * more, patient pass, and after a definite refusal the error says why. Any + * other inline failure is rethrown unchanged. * * With the `"links-denied"` hint the inline run goes first, and Cloud Fetch is * tried only if that cannot deliver. @@ -451,8 +458,8 @@ export async function* deliverArrowBytesWithFallback( return; } - const status = downloadError.context?.httpStatus; - const refused = status === 400 || status === 403; + const refused = isRefusal(downloadError); + const unanswered = downloadError.context?.timedOut === true; if (refused) opts?.onCapabilityResolved?.("links-denied"); const size = out.cloudFetch?.totalByteCount; tooLarge ||= size !== undefined && size > MAX_INLINE_ATTACHMENT_BYTES; @@ -465,6 +472,7 @@ export async function* deliverArrowBytesWithFallback( signal, ); if ("bytes" in inline) { + if (unanswered) opts?.onCapabilityResolved?.("links-denied"); yield inline.bytes; return; } @@ -480,6 +488,9 @@ export async function* deliverArrowBytesWithFallback( retried = await patient.next(); } catch (err: unknown) { if (signal?.aborted || !isDownloadFailure(err)) throw err; + if (isRefusal(err) || err.context?.timedOut === true) { + opts?.onCapabilityResolved?.("links-denied"); + } throw linksDenied(err); } if (hinted) opts?.onCapabilityResolved?.("external"); diff --git a/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fallback.integration.test.ts b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fallback.integration.test.ts index 39d1c39dc..22776ce22 100644 --- a/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fallback.integration.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fallback.integration.test.ts @@ -216,7 +216,7 @@ describe("Arrow charts when workspace storage refuses the Cloud Fetch download", expect(storage.gets()).toBe(getsAfterFirst + 1); }); - test("storage that never answers falls back, and the next chart tries storage again", async () => { + test("storage that never answers falls back, and the next chart skips storage", async () => { await setStorage({ kind: "hang" }); await boot({ timeout: 300 }); installWarehouse(); @@ -226,8 +226,8 @@ describe("Arrow charts when workspace storage refuses the Cloud Fetch download", warehouse.executions.length = 0; await expectNativeArrow(await chart("stalled-again")); - expect(warehouse.executions).toContain("EXTERNAL_LINKS+ARROW_STREAM"); - expect(storage.gets()).toBe(2); + expect(warehouse.executions).toEqual(["INLINE+JSON_ARRAY"]); + expect(storage.gets()).toBe(1); }); test("a body that stalls after the headers falls back instead of hanging", async () => { diff --git a/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts b/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts index 14025074e..07946951d 100644 --- a/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts @@ -573,8 +573,18 @@ describe("deliverArrowBytesWithFallback", () => { }, ); + test("storage that never answers falls back and is remembered as denied", async () => { + const ctx = await setup({ storage: { kind: "hang" } }); + const { bytes, reported, error } = await run(ctx); + + expect(error).toBeUndefined(); + expectNativeArrow(ctx, bytes); + expect(ctx.storage.gets()).toBe(1); + expect(ctx.executions.at(-1)).toBe("INLINE+JSON_ARRAY"); + expect(reported.at(-1)).toBe("links-denied"); + }); + test.each([ - ["no answer", { kind: "hang" }], ["headers then no body", { kind: "hang-after-headers" }], ["headers then a reset", { kind: "reset-after-headers" }], ] as const)( @@ -662,7 +672,7 @@ describe("deliverArrowBytesWithFallback", () => { expect(error?.errorCode).toBe("EXTERNAL_LINKS_DENIED"); expect(ctx.storage.gets()).toBe(2); expect(ctx.executions).not.toContain("INLINE+JSON_ARRAY"); - expect(reported).not.toContain("links-denied"); + expect(reported.at(-1)).toBe("links-denied"); }); test("an inline result that spans several chunks is not served truncated", async () => { diff --git a/packages/appkit/src/stream/arrow-stream-processor.ts b/packages/appkit/src/stream/arrow-stream-processor.ts index 822de4a88..2021eddf9 100644 --- a/packages/appkit/src/stream/arrow-stream-processor.ts +++ b/packages/appkit/src/stream/arrow-stream-processor.ts @@ -44,10 +44,11 @@ function downloadFailed( message: string, httpStatus?: number, cause?: unknown, + timedOut = false, ): ExecutionError { return new ExecutionError(message, { errorCode: EXTERNAL_LINK_DOWNLOAD_FAILED, - context: { httpStatus }, + context: { httpStatus, timedOut }, cause: cause instanceof Error ? cause : undefined, }); } @@ -192,6 +193,7 @@ export class ArrowStreamProcessor { `Failed to download chunk ${chunk.chunkIndex}: ${reason}`, failure?.status, failure?.error, + failure?.timedOut, ); } From 2eaaf1127a3225bbdbcde83d263d24772769762a Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 9 Nov 2026 18:33:58 +0000 Subject: [PATCH 4/9] fix(appkit): rebuild Arrow from JSON_ARRAY cells through one tested codec table Each scalar type's JSON_ARRAY parser now sits next to its formatter in json-array-cells.ts. The formatters move there unchanged from result-delivery.ts. Cells that don't fit their type now throw instead of passing through altered: a DECIMAL with more fraction digits than its scale (was silently cut), a TIMESTAMP without an offset, and a TIMESTAMP_NTZ with one. The rebuilt stream names its columns like the native Cloud Fetch stream, duplicates included. The browser skips relabeling when names repeat, so col_N names reached the user. buildArrowIPCFromRows takes the SDK's ColumnInfo. tests/fixtures/typed-values.json is a captured warehouse response, with the JSON_ARRAY text and the native Arrow stream, that pins the formats. Signed-off-by: Pedro Figueiredo --- .../connectors/sql-warehouse/arrow-schema.ts | 180 ++--- .../sql-warehouse/json-array-cells.ts | 151 ++++ .../sql-warehouse/tests/arrow-schema.test.ts | 95 ++- .../tests/fixtures/typed-values.json | 683 ++++++++++++++++++ .../tests/json-array-cells.test.ts | 218 ++++++ .../src/plugins/analytics/result-delivery.ts | 77 +- .../analytics/tests/cloud-fetch-fixtures.ts | 20 +- 7 files changed, 1207 insertions(+), 217 deletions(-) create mode 100644 packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts create mode 100644 packages/appkit/src/connectors/sql-warehouse/tests/fixtures/typed-values.json create mode 100644 packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts diff --git a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts index 8193a074e..806e8f238 100644 --- a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts +++ b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts @@ -16,6 +16,7 @@ import { List, Map_, Null, + RecordBatch, Schema, Struct, Table, @@ -23,12 +24,14 @@ import { tableToIPC, Type, Utf8, - type Vector, makeData, makeVector, vectorFromArray, } from "apache-arrow"; +import type { ColumnInfo } from "../../workspace-client"; +import { hasJsonArrayCodec, parseJsonArrayCell } from "./json-array-cells"; + /** * Parse a Databricks SQL type text (the value returned by the Statement * Execution API in `ColumnInfo.type_text`) into an Apache Arrow DataType. @@ -56,13 +59,7 @@ export function parseDatabricksType(typeText: string): DataType { * deliver a real Arrow Table to the client, preserving the hook's typed * contract. */ -export function buildEmptyArrowIPCBase64( - columns: Array<{ - name?: string; - typeText?: string; - typeName?: string; - }>, -): string { +export function buildEmptyArrowIPCBase64(columns: ColumnInfo[]): string { const fields = columns.map((col, index) => { const typeText = col.typeText ?? col.typeName ?? "STRING"; let dataType: DataType; @@ -80,77 +77,58 @@ export function buildEmptyArrowIPCBase64( return Buffer.from(ipc).toString("base64"); } -/** Column metadata from `manifest.schema.columns`. */ -export interface ArrowRowColumn { - name?: string; - typeText?: string; - typeName?: string; - typePrecision?: number; - typeScale?: number; -} - /** * Rebuild the Arrow IPC stream a warehouse serves for `ARROW_STREAM` from the - * positional string rows of an `INLINE` + `JSON_ARRAY` result. Fields are named - * `col_N` like the native stream, and each type is what - * {@link parseDatabricksType} gives that column, with DECIMAL precision and - * scale taken from `typePrecision` and `typeScale`. + * positional string rows of an `INLINE` + `JSON_ARRAY` result. Fields carry the + * manifest column names, duplicates included, like the native Cloud Fetch + * stream, and each type is what {@link parseDatabricksType} gives that column, + * with DECIMAL precision and scale taken from `typePrecision` and `typeScale`. * * Nested, interval, and geospatial columns arrive as strings on the JSON wire - * and stay strings. A cell that does not parse as its column type throws. + * and stay strings, and so does any type without a {@link parseJsonArrayCell} + * codec. A cell that does not parse as its column type throws. */ export function buildArrowIPCFromRows( - columns: ArrowRowColumn[], + columns: ColumnInfo[], rows: Array>, ): Uint8Array { - const vectors: Record = {}; - columns.forEach((col, index) => { - const cells = rows.map((row) => row[index] ?? null); - vectors[`col_${index}`] = vectorFromCells(col, cells); - }); - return tableToIPC(new Table(vectors), "stream"); + const vectors = columns.map((col, index) => + vectorFromCells( + col, + rows.map((row) => row[index] ?? null), + ), + ); + const fields = vectors.map( + (vector, index) => + new Field(columns[index].name || `col_${index}`, vector.type, true), + ); + const batch = new RecordBatch( + new Schema(fields), + makeData({ + type: new Struct(fields), + length: rows.length, + nullCount: 0, + children: vectors.map((vector) => vector.data[0]), + }), + ); + return tableToIPC(new Table(batch), "stream"); } -function vectorFromCells(col: ArrowRowColumn, cells: Array) { +function vectorFromCells(col: ColumnInfo, cells: Array) { const type = rebuildType(col); - const map = (parse: (cell: string) => T) => - cells.map((cell) => (cell === null ? null : parse(cell))); - switch (type.typeId) { - case Type.Int: { - const { bitWidth } = type as unknown as { bitWidth: number }; - return vectorFromArray( - bitWidth === 64 ? map(parseBigInt) : map(parseInteger), - type as never, - ); - } - case Type.Float: - return vectorFromArray(map(parseFloatCell), type as never); - case Type.Bool: - return vectorFromArray(map(parseBoolean), type as never); - case Type.Date: - return vectorFromArray(map(parseUtcMillis), type as never); - case Type.Timestamp: - return vectorFromArray(map(parseUtcMillis), type as never); - case Type.Binary: - return vectorFromArray( - map((cell) => new Uint8Array(Buffer.from(cell, "base64"))), - type as never, - ); - case Type.Decimal: { - const { scale, precision } = type as unknown as { - scale: number; - precision: number; - }; - return decimalVector(cells, scale, precision); - } - case Type.Null: - return vectorFromArray( - cells.map(() => null), - type as never, - ); - default: - return vectorFromArray(cells, new Utf8()); + if (type.typeId === Type.Null) { + return vectorFromArray( + cells.map(() => null), + type as never, + ); + } + const values = cells.map((cell) => + cell === null ? null : parseJsonArrayCell(cell, type), + ); + if (type.typeId === Type.Decimal) { + return decimalVector(values as Array, type as Decimal); } + return vectorFromArray(values, type as never); } const STRING_ONLY_TYPES = new Set([ @@ -161,7 +139,7 @@ const STRING_ONLY_TYPES = new Set([ Type.Duration, ]); -function rebuildType(col: ArrowRowColumn): DataType { +function rebuildType(col: ColumnInfo): DataType { const typeText = col.typeText ?? col.typeName ?? "STRING"; let type: DataType; try { @@ -177,68 +155,22 @@ function rebuildType(col: ArrowRowColumn): DataType { if (type.typeId === Type.Decimal && col.typePrecision != null) { return new Decimal(col.typeScale ?? 0, col.typePrecision, 128); } + if (type.typeId !== Type.Null && !hasJsonArrayCodec(type)) return new Utf8(); return type; } -function parseInteger(cell: string): number { - const n = Number(cell); - if (cell.trim() === "" || !Number.isInteger(n)) { - throw new Error(`Invalid integer "${cell}"`); - } - return n; -} - -function parseBigInt(cell: string): bigint { - if (cell.trim() === "") throw new Error(`Invalid integer "${cell}"`); - return BigInt(cell); -} - -function parseFloatCell(cell: string): number { - const n = Number(cell); - if (cell.trim() === "" || (Number.isNaN(n) && cell !== "NaN")) { - throw new Error(`Invalid number "${cell}"`); - } - return n; -} - -function parseBoolean(cell: string): boolean { - if (cell !== "true" && cell !== "false") { - throw new Error(`Invalid boolean "${cell}"`); - } - return cell === "true"; -} - -/** Epoch ms for an ISO date or timestamp; a value with no zone is UTC. */ -function parseUtcMillis(cell: string): number { - const ms = Date.parse( - /(?:Z|[+-]\d\d(?::?\d\d)?)$/i.test(cell) || !cell.includes("T") - ? cell - : `${cell}Z`, - ); - if (Number.isNaN(ms)) throw new Error(`Invalid date or timestamp "${cell}"`); - return ms; -} - -function decimalVector( - cells: Array, - scale: number, - precision: number, -) { - const words = new Uint32Array(cells.length * 4); - const validity = new Uint8Array(Math.ceil(cells.length / 8)); +/** `vectorFromArray` cannot build Decimal128, so write the 128-bit words directly. */ +function decimalVector(unscaled: Array, type: Decimal) { + const words = new Uint32Array(unscaled.length * 4); + const validity = new Uint8Array(Math.ceil(unscaled.length / 8)); let nullCount = 0; - cells.forEach((cell, i) => { - if (cell === null) { + unscaled.forEach((value, i) => { + if (value === null) { nullCount++; return; } validity[i >> 3] |= 1 << (i & 7); - const negative = cell.startsWith("-"); - const [whole, fraction = ""] = cell.replace(/^[-+]/, "").split("."); - const digits = `${whole}${fraction.padEnd(scale, "0").slice(0, scale)}`; - if (!/^\d+$/.test(digits)) throw new Error(`Invalid decimal "${cell}"`); - const unscaled = BigInt(digits); - const twosComplement = negative ? (1n << 128n) - unscaled : unscaled; + const twosComplement = value < 0n ? (1n << 128n) + value : value; for (let w = 0; w < 4; w++) { words[i * 4 + w] = Number( (twosComplement >> BigInt(32 * w)) & 0xffffffffn, @@ -247,8 +179,8 @@ function decimalVector( }); return makeVector( makeData({ - type: new Decimal(scale, precision, 128), - length: cells.length, + type: new Decimal(type.scale, type.precision, 128), + length: unscaled.length, nullCount, nullBitmap: validity, data: words, diff --git a/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts b/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts new file mode 100644 index 000000000..e08d91a9d --- /dev/null +++ b/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts @@ -0,0 +1,151 @@ +import { type DataType, Type } from "apache-arrow"; + +/** + * How one scalar type is written in an `INLINE` + `JSON_ARRAY` result. + * `format` turns an Arrow value into the warehouse's text, and `parse` turns + * that text back into the value `vectorFromArray` accepts (an unscaled `bigint` + * for DECIMAL, epoch milliseconds for DATE and TIMESTAMP). The text forms are + * pinned to a captured warehouse response in `tests/fixtures/typed-values.json`. + */ +interface CellCodec { + format(value: unknown, type: DataType): string; + parse(cell: string, type: DataType): unknown; +} + +const DECIMAL_TEXT = /^(-?)(\d+)(?:\.(\d+))?$/; +const DATE_TEXT = /^\d{4}-\d{2}-\d{2}$/; +/** `TIMESTAMP` cells carry the session offset (or `Z`); `TIMESTAMP_NTZ` cells carry none. */ +const ZONED_TIMESTAMP_TEXT = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/; +const LOCAL_TIMESTAMP_TEXT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/; + +function invalid(kind: string, cell: string): Error { + return new Error(`Invalid ${kind} "${cell}"`); +} + +function epochMillis(text: string, kind: string, cell: string): number { + const ms = Date.parse(text); + if (Number.isNaN(ms)) throw invalid(kind, cell); + return ms; +} + +const hasTimezone = (type: DataType) => + (type as DataType & { timezone?: string | null }).timezone != null; + +const CODECS: Partial> = { + [Type.Int]: { + format: (value) => String(value), + parse(cell, type) { + const { bitWidth } = type as DataType & { bitWidth: number }; + if (bitWidth === 64) { + let n: bigint; + try { + n = BigInt(cell); + } catch { + throw invalid("integer", cell); + } + if (String(n) !== cell) throw invalid("integer", cell); + return n; + } + const n = Number(cell); + const limit = 2 ** (bitWidth - 1); + if ( + String(n) !== cell || + !Number.isInteger(n) || + n < -limit || + n >= limit + ) { + throw invalid("integer", cell); + } + return n; + }, + }, + [Type.Float]: { + format: (value) => String(value), + parse(cell) { + const n = Number(cell); + if (cell.trim() === "" || (Number.isNaN(n) && cell !== "NaN")) { + throw invalid("number", cell); + } + return n; + }, + }, + [Type.Decimal]: { + format(value, type) { + const { scale } = type as DataType & { scale: number }; + const unscaled = String(value); + if (scale <= 0) return unscaled; + const negative = unscaled.startsWith("-"); + let digits = negative ? unscaled.slice(1) : unscaled; + if (digits.length <= scale) digits = digits.padStart(scale + 1, "0"); + const point = digits.length - scale; + const out = `${digits.slice(0, point)}.${digits.slice(point)}`; + return negative ? `-${out}` : out; + }, + parse(cell, type) { + const { scale } = type as DataType & { scale: number }; + const match = DECIMAL_TEXT.exec(cell); + const [, sign, whole, fraction = ""] = match ?? []; + if (!match || fraction.length > scale) throw invalid("decimal", cell); + const unscaled = BigInt(`${whole}${fraction.padEnd(scale, "0")}`); + return sign === "-" ? -unscaled : unscaled; + }, + }, + [Type.Date]: { + format: (value) => new Date(Number(value)).toISOString().slice(0, 10), + parse(cell) { + if (!DATE_TEXT.test(cell)) throw invalid("date", cell); + return epochMillis(`${cell}T00:00:00Z`, "date", cell); + }, + }, + [Type.Timestamp]: { + format(value, type) { + const iso = new Date(Number(value)).toISOString(); + return hasTimezone(type) ? iso : iso.slice(0, -1); + }, + parse(cell, type) { + if (hasTimezone(type)) { + if (!ZONED_TIMESTAMP_TEXT.test(cell)) throw invalid("timestamp", cell); + return epochMillis(cell, "timestamp", cell); + } + if (!LOCAL_TIMESTAMP_TEXT.test(cell)) throw invalid("timestamp", cell); + return epochMillis(`${cell}Z`, "timestamp", cell); + }, + }, + [Type.Bool]: { + format: (value) => String(value), + parse(cell) { + if (cell !== "true" && cell !== "false") throw invalid("boolean", cell); + return cell === "true"; + }, + }, + [Type.Binary]: { + format: (value) => Buffer.from(value as Uint8Array).toString("base64"), + parse: (cell) => new Uint8Array(Buffer.from(cell, "base64")), + }, + [Type.Utf8]: { + format: (value) => String(value), + parse: (cell) => cell, + }, +}; + +/** Whether {@link parseJsonArrayCell} can read cells of this Arrow type. */ +export function hasJsonArrayCodec(type: DataType): boolean { + return CODECS[type.typeId] !== undefined; +} + +/** Render a scalar Arrow value the way the warehouse writes it under `JSON_ARRAY`. */ +export function formatJsonArrayCell(value: unknown, type: DataType): string { + const codec = CODECS[type.typeId]; + if (codec) return codec.format(value, type); + if (value instanceof Uint8Array) return Buffer.from(value).toString("base64"); + if (value instanceof Date) return value.toISOString(); + return String(value); +} + +/** Read a `JSON_ARRAY` cell as a value of `type`. Throws when the text does not fit the type. */ +export function parseJsonArrayCell(cell: string, type: DataType): unknown { + const codec = CODECS[type.typeId]; + if (!codec) throw new Error(`No JSON_ARRAY codec for ${String(type)}`); + return codec.parse(cell, type); +} diff --git a/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts b/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts index 65b45a234..6effa1078 100644 --- a/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts +++ b/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts @@ -1,3 +1,5 @@ +import { readFileSync } from "node:fs"; + import { Binary, Bool, @@ -23,6 +25,7 @@ import { } from "apache-arrow"; import { describe, expect, test } from "vitest"; +import type { ColumnInfo } from "../../../workspace-client"; import { buildArrowIPCFromRows, buildEmptyArrowIPCBase64, @@ -639,7 +642,7 @@ describe("buildArrowIPCFromRows", () => { const native = parseDatabricksType(column.typeText ?? "STRING"); const field = table.schema.fields[0]; - expect(field.name).toBe("col_0"); + expect(field.name).toBe(column.name); expect(field.typeId).toBe(native.typeId); if (column.typeName === "DECIMAL") { const type = field.type as Decimal; @@ -650,12 +653,12 @@ describe("buildArrowIPCFromRows", () => { } else { expect(String(field.type)).toBe(String(native)); } - const values = [...(table.getChild("col_0") as Iterable)]; + const values = [...(table.getChildAt(0) as Iterable)]; expect(values.map((v) => (read ? read(v) : v))).toEqual(expected); }, ); - test("positional rows keep duplicate column names and JSON-looking strings verbatim", () => { + test("duplicate column names and JSON-looking strings come through verbatim", () => { const payload = '{"id":9007199254740993, "label":"kept verbatim"}'; const ipc = buildArrowIPCFromRows( [ @@ -667,13 +670,13 @@ describe("buildArrowIPCFromRows", () => { ); const table = tableFromIPC(ipc); expect(table.schema.fields.map((f) => f.name)).toEqual([ - "col_0", - "col_1", - "col_2", + "x", + "x", + "payload", ]); - expect(table.getChild("col_0")?.get(0)).toBe(1); - expect(table.getChild("col_1")?.get(0)).toBe(2); - expect(table.getChild("col_2")?.get(0)).toBe(payload); + expect(table.getChildAt(0)?.get(0)).toBe(1); + expect(table.getChildAt(1)?.get(0)).toBe(2); + expect(table.getChildAt(2)?.get(0)).toBe(payload); }); test("nested, interval and geospatial columns stay strings", () => { @@ -696,11 +699,11 @@ describe("buildArrowIPCFromRows", () => { columns.map(() => Type.Utf8), ); expect(table.toArray()[0].toJSON()).toEqual({ - col_0: row[0], - col_1: row[1], - col_2: row[2], - col_3: row[3], - col_4: row[4], + a: row[0], + m: row[1], + s: row[2], + i: row[3], + g: row[4], }); }); @@ -742,3 +745,67 @@ describe("buildArrowIPCFromRows", () => { ).toThrow(/Invalid boolean/); }); }); + +describe("buildArrowIPCFromRows against a captured warehouse response", () => { + const fixture = JSON.parse( + readFileSync( + new URL("./fixtures/typed-values.json", import.meta.url), + "utf8", + ), + ) as { + columns: ColumnInfo[]; + dataArray: Array>; + arrowIpcBase64: string; + }; + const native = tableFromIPC(Buffer.from(fixture.arrowIpcBase64, "base64")); + const rebuilt = tableFromIPC( + buildArrowIPCFromRows(fixture.columns, fixture.dataArray), + ); + const comparable = (value: unknown) => + value !== null && typeof value === "object" ? String(value) : value; + const values = (table: typeof native, index: number) => + [...(table.getChildAt(index) ?? [])].map(comparable); + const SESSION_ZONE = "America/Los_Angeles"; + + test("has the same rows and column names, duplicates included", () => { + expect(rebuilt.numRows).toBe(native.numRows); + expect(rebuilt.schema.fields.map((f) => f.name)).toEqual( + native.schema.fields.map((f) => f.name), + ); + }); + + test.each( + fixture.columns + .map((column, index) => [column.name ?? "", index] as const) + .filter(([name]) => !["ts", "ts_micro", "arr"].includes(name)), + )("column %s (%i) has the native Arrow type and values", (_name, index) => { + expect(String(rebuilt.schema.fields[index].type)).toBe( + String(native.schema.fields[index].type), + ); + expect(values(rebuilt, index)).toEqual(values(native, index)); + }); + + test("TIMESTAMP keeps every instant but is labeled UTC instead of the session zone", () => { + const index = fixture.columns.findIndex((c) => c.name === "ts"); + expect(native.schema.fields[index].type.timezone).toBe(SESSION_ZONE); + expect(rebuilt.schema.fields[index].type.timezone).toBe("UTC"); + expect(values(rebuilt, index)).toEqual(values(native, index)); + }); + + test("TIMESTAMP keeps milliseconds, because the JSON text has no microseconds", () => { + const index = fixture.columns.findIndex((c) => c.name === "ts_micro"); + expect(fixture.dataArray[0][index]).toMatch(/\.\d{3}-\d{2}:\d{2}$/); + expect(values(rebuilt, index)).toEqual( + values(native, index).map((ms) => Math.trunc(ms as number)), + ); + }); + + test("ARRAY arrives as JSON text and stays a string column", () => { + const index = fixture.columns.findIndex((c) => c.name === "arr"); + expect(native.schema.fields[index].type.typeId).toBe(Type.List); + expect(rebuilt.schema.fields[index].type.typeId).toBe(Type.Utf8); + expect(values(rebuilt, index)).toEqual( + fixture.dataArray.map((row) => row[index]), + ); + }); +}); diff --git a/packages/appkit/src/connectors/sql-warehouse/tests/fixtures/typed-values.json b/packages/appkit/src/connectors/sql-warehouse/tests/fixtures/typed-values.json new file mode 100644 index 000000000..57fb0d722 --- /dev/null +++ b/packages/appkit/src/connectors/sql-warehouse/tests/fixtures/typed-values.json @@ -0,0 +1,683 @@ +{ + "source": "Statement Execution API on a serverless PRO SQL warehouse (session time zone America/Los_Angeles), captured 2026-11-09", + "statement": "SELECT\n CAST(id AS INT) AS id,\n CAST(id AS BIGINT) * 1000000000007 AS big,\n CAST(id % 100 AS TINYINT) AS tiny,\n CAST(id AS SMALLINT) AS small,\n CAST(id * 1.5 AS DOUBLE) AS amount,\n CAST(id * 0.25 AS FLOAT) AS ratio,\n CAST((id * 101 - 50) / 100.0 AS DECIMAL(10, 2)) AS price,\n DATE_ADD(DATE'2026-01-01', CAST(id AS INT)) AS day,\n TIMESTAMPADD(HOUR, id, TIMESTAMP'2026-01-01 10:00:00') AS ts,\n TIMESTAMPADD(HOUR, id, TIMESTAMP_NTZ'2026-01-01 10:00:00') AS ts_ntz,\n TIMESTAMPADD(MICROSECOND, id, TIMESTAMP'2026-01-01 10:00:00.123456') AS ts_micro,\n CONCAT('{\"order_id\": 1234567890123456789', CAST(id % 10 AS STRING), ', \"tags\": [1, 2]}') AS payload,\n id % 2 = 0 AS flag,\n CASE WHEN id % 7 = 3 THEN NULL ELSE CONCAT('row-', CAST(id AS STRING)) END AS label,\n ARRAY(id, id + 1) AS arr,\n id AS dup,\n id + 1 AS dup\nFROM range(0, 30) AS t(id)\nORDER BY id", + "columns": [ + { + "name": "id", + "typeName": "INT", + "typeText": "INT", + "position": 0 + }, + { + "name": "big", + "typeName": "LONG", + "typeText": "BIGINT", + "position": 1 + }, + { + "name": "tiny", + "typeName": "BYTE", + "typeText": "TINYINT", + "position": 2 + }, + { + "name": "small", + "typeName": "SHORT", + "typeText": "SMALLINT", + "position": 3 + }, + { + "name": "amount", + "typeName": "DOUBLE", + "typeText": "DOUBLE", + "position": 4 + }, + { + "name": "ratio", + "typeName": "FLOAT", + "typeText": "FLOAT", + "position": 5 + }, + { + "name": "price", + "typeName": "DECIMAL", + "typeText": "DECIMAL(10,2)", + "position": 6, + "typePrecision": 10, + "typeScale": 2 + }, + { + "name": "day", + "typeName": "DATE", + "typeText": "DATE", + "position": 7 + }, + { + "name": "ts", + "typeName": "TIMESTAMP", + "typeText": "TIMESTAMP", + "position": 8 + }, + { + "name": "ts_ntz", + "typeName": "TIMESTAMP_NTZ", + "typeText": "TIMESTAMP_NTZ", + "position": 9 + }, + { + "name": "ts_micro", + "typeName": "TIMESTAMP", + "typeText": "TIMESTAMP", + "position": 10 + }, + { + "name": "payload", + "typeName": "STRING", + "typeText": "STRING", + "position": 11 + }, + { + "name": "flag", + "typeName": "BOOLEAN", + "typeText": "BOOLEAN", + "position": 12 + }, + { + "name": "label", + "typeName": "STRING", + "typeText": "STRING", + "position": 13 + }, + { + "name": "arr", + "typeName": "ARRAY", + "typeText": "ARRAY", + "position": 14 + }, + { + "name": "dup", + "typeName": "LONG", + "typeText": "BIGINT", + "position": 15 + }, + { + "name": "dup", + "typeName": "LONG", + "typeText": "BIGINT", + "position": 16 + } + ], + "dataArray": [ + [ + "0", + "0", + "0", + "0", + "0.0", + "0.0", + "-0.50", + "2026-01-01", + "2026-01-01T10:00:00.000-08:00", + "2026-01-01T10:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567890, \"tags\": [1, 2]}", + "true", + "row-0", + "[\"0\",\"1\"]", + "0", + "1" + ], + [ + "1", + "1000000000007", + "1", + "1", + "1.5", + "0.25", + "0.51", + "2026-01-02", + "2026-01-01T11:00:00.000-08:00", + "2026-01-01T11:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567891, \"tags\": [1, 2]}", + "false", + "row-1", + "[\"1\",\"2\"]", + "1", + "2" + ], + [ + "2", + "2000000000014", + "2", + "2", + "3.0", + "0.5", + "1.52", + "2026-01-03", + "2026-01-01T12:00:00.000-08:00", + "2026-01-01T12:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567892, \"tags\": [1, 2]}", + "true", + "row-2", + "[\"2\",\"3\"]", + "2", + "3" + ], + [ + "3", + "3000000000021", + "3", + "3", + "4.5", + "0.75", + "2.53", + "2026-01-04", + "2026-01-01T13:00:00.000-08:00", + "2026-01-01T13:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567893, \"tags\": [1, 2]}", + "false", + null, + "[\"3\",\"4\"]", + "3", + "4" + ], + [ + "4", + "4000000000028", + "4", + "4", + "6.0", + "1.0", + "3.54", + "2026-01-05", + "2026-01-01T14:00:00.000-08:00", + "2026-01-01T14:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567894, \"tags\": [1, 2]}", + "true", + "row-4", + "[\"4\",\"5\"]", + "4", + "5" + ], + [ + "5", + "5000000000035", + "5", + "5", + "7.5", + "1.25", + "4.55", + "2026-01-06", + "2026-01-01T15:00:00.000-08:00", + "2026-01-01T15:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567895, \"tags\": [1, 2]}", + "false", + "row-5", + "[\"5\",\"6\"]", + "5", + "6" + ], + [ + "6", + "6000000000042", + "6", + "6", + "9.0", + "1.5", + "5.56", + "2026-01-07", + "2026-01-01T16:00:00.000-08:00", + "2026-01-01T16:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567896, \"tags\": [1, 2]}", + "true", + "row-6", + "[\"6\",\"7\"]", + "6", + "7" + ], + [ + "7", + "7000000000049", + "7", + "7", + "10.5", + "1.75", + "6.57", + "2026-01-08", + "2026-01-01T17:00:00.000-08:00", + "2026-01-01T17:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567897, \"tags\": [1, 2]}", + "false", + "row-7", + "[\"7\",\"8\"]", + "7", + "8" + ], + [ + "8", + "8000000000056", + "8", + "8", + "12.0", + "2.0", + "7.58", + "2026-01-09", + "2026-01-01T18:00:00.000-08:00", + "2026-01-01T18:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567898, \"tags\": [1, 2]}", + "true", + "row-8", + "[\"8\",\"9\"]", + "8", + "9" + ], + [ + "9", + "9000000000063", + "9", + "9", + "13.5", + "2.25", + "8.59", + "2026-01-10", + "2026-01-01T19:00:00.000-08:00", + "2026-01-01T19:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567899, \"tags\": [1, 2]}", + "false", + "row-9", + "[\"9\",\"10\"]", + "9", + "10" + ], + [ + "10", + "10000000000070", + "10", + "10", + "15.0", + "2.5", + "9.60", + "2026-01-11", + "2026-01-01T20:00:00.000-08:00", + "2026-01-01T20:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567890, \"tags\": [1, 2]}", + "true", + null, + "[\"10\",\"11\"]", + "10", + "11" + ], + [ + "11", + "11000000000077", + "11", + "11", + "16.5", + "2.75", + "10.61", + "2026-01-12", + "2026-01-01T21:00:00.000-08:00", + "2026-01-01T21:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567891, \"tags\": [1, 2]}", + "false", + "row-11", + "[\"11\",\"12\"]", + "11", + "12" + ], + [ + "12", + "12000000000084", + "12", + "12", + "18.0", + "3.0", + "11.62", + "2026-01-13", + "2026-01-01T22:00:00.000-08:00", + "2026-01-01T22:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567892, \"tags\": [1, 2]}", + "true", + "row-12", + "[\"12\",\"13\"]", + "12", + "13" + ], + [ + "13", + "13000000000091", + "13", + "13", + "19.5", + "3.25", + "12.63", + "2026-01-14", + "2026-01-01T23:00:00.000-08:00", + "2026-01-01T23:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567893, \"tags\": [1, 2]}", + "false", + "row-13", + "[\"13\",\"14\"]", + "13", + "14" + ], + [ + "14", + "14000000000098", + "14", + "14", + "21.0", + "3.5", + "13.64", + "2026-01-15", + "2026-01-02T00:00:00.000-08:00", + "2026-01-02T00:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567894, \"tags\": [1, 2]}", + "true", + "row-14", + "[\"14\",\"15\"]", + "14", + "15" + ], + [ + "15", + "15000000000105", + "15", + "15", + "22.5", + "3.75", + "14.65", + "2026-01-16", + "2026-01-02T01:00:00.000-08:00", + "2026-01-02T01:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567895, \"tags\": [1, 2]}", + "false", + "row-15", + "[\"15\",\"16\"]", + "15", + "16" + ], + [ + "16", + "16000000000112", + "16", + "16", + "24.0", + "4.0", + "15.66", + "2026-01-17", + "2026-01-02T02:00:00.000-08:00", + "2026-01-02T02:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567896, \"tags\": [1, 2]}", + "true", + "row-16", + "[\"16\",\"17\"]", + "16", + "17" + ], + [ + "17", + "17000000000119", + "17", + "17", + "25.5", + "4.25", + "16.67", + "2026-01-18", + "2026-01-02T03:00:00.000-08:00", + "2026-01-02T03:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567897, \"tags\": [1, 2]}", + "false", + null, + "[\"17\",\"18\"]", + "17", + "18" + ], + [ + "18", + "18000000000126", + "18", + "18", + "27.0", + "4.5", + "17.68", + "2026-01-19", + "2026-01-02T04:00:00.000-08:00", + "2026-01-02T04:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567898, \"tags\": [1, 2]}", + "true", + "row-18", + "[\"18\",\"19\"]", + "18", + "19" + ], + [ + "19", + "19000000000133", + "19", + "19", + "28.5", + "4.75", + "18.69", + "2026-01-20", + "2026-01-02T05:00:00.000-08:00", + "2026-01-02T05:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567899, \"tags\": [1, 2]}", + "false", + "row-19", + "[\"19\",\"20\"]", + "19", + "20" + ], + [ + "20", + "20000000000140", + "20", + "20", + "30.0", + "5.0", + "19.70", + "2026-01-21", + "2026-01-02T06:00:00.000-08:00", + "2026-01-02T06:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567890, \"tags\": [1, 2]}", + "true", + "row-20", + "[\"20\",\"21\"]", + "20", + "21" + ], + [ + "21", + "21000000000147", + "21", + "21", + "31.5", + "5.25", + "20.71", + "2026-01-22", + "2026-01-02T07:00:00.000-08:00", + "2026-01-02T07:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567891, \"tags\": [1, 2]}", + "false", + "row-21", + "[\"21\",\"22\"]", + "21", + "22" + ], + [ + "22", + "22000000000154", + "22", + "22", + "33.0", + "5.5", + "21.72", + "2026-01-23", + "2026-01-02T08:00:00.000-08:00", + "2026-01-02T08:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567892, \"tags\": [1, 2]}", + "true", + "row-22", + "[\"22\",\"23\"]", + "22", + "23" + ], + [ + "23", + "23000000000161", + "23", + "23", + "34.5", + "5.75", + "22.73", + "2026-01-24", + "2026-01-02T09:00:00.000-08:00", + "2026-01-02T09:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567893, \"tags\": [1, 2]}", + "false", + "row-23", + "[\"23\",\"24\"]", + "23", + "24" + ], + [ + "24", + "24000000000168", + "24", + "24", + "36.0", + "6.0", + "23.74", + "2026-01-25", + "2026-01-02T10:00:00.000-08:00", + "2026-01-02T10:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567894, \"tags\": [1, 2]}", + "true", + null, + "[\"24\",\"25\"]", + "24", + "25" + ], + [ + "25", + "25000000000175", + "25", + "25", + "37.5", + "6.25", + "24.75", + "2026-01-26", + "2026-01-02T11:00:00.000-08:00", + "2026-01-02T11:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567895, \"tags\": [1, 2]}", + "false", + "row-25", + "[\"25\",\"26\"]", + "25", + "26" + ], + [ + "26", + "26000000000182", + "26", + "26", + "39.0", + "6.5", + "25.76", + "2026-01-27", + "2026-01-02T12:00:00.000-08:00", + "2026-01-02T12:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567896, \"tags\": [1, 2]}", + "true", + "row-26", + "[\"26\",\"27\"]", + "26", + "27" + ], + [ + "27", + "27000000000189", + "27", + "27", + "40.5", + "6.75", + "26.77", + "2026-01-28", + "2026-01-02T13:00:00.000-08:00", + "2026-01-02T13:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567897, \"tags\": [1, 2]}", + "false", + "row-27", + "[\"27\",\"28\"]", + "27", + "28" + ], + [ + "28", + "28000000000196", + "28", + "28", + "42.0", + "7.0", + "27.78", + "2026-01-29", + "2026-01-02T14:00:00.000-08:00", + "2026-01-02T14:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567898, \"tags\": [1, 2]}", + "true", + "row-28", + "[\"28\",\"29\"]", + "28", + "29" + ], + [ + "29", + "29000000000203", + "29", + "29", + "43.5", + "7.25", + "28.79", + "2026-01-30", + "2026-01-02T15:00:00.000-08:00", + "2026-01-02T15:00:00.000", + "2026-01-01T10:00:00.123-08:00", + "{\"order_id\": 12345678901234567899, \"tags\": [1, 2]}", + "false", + "row-29", + "[\"29\",\"30\"]", + "29", + "30" + ] + ], + "arrowIpcBase64": "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" +} diff --git a/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts b/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts new file mode 100644 index 000000000..19ec1fe11 --- /dev/null +++ b/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts @@ -0,0 +1,218 @@ +import { + Binary, + Bool, + type DataType, + DateDay, + Decimal, + Float32, + Float64, + Int8, + Int16, + Int32, + Int64, + makeData, + makeVector, + Table, + TimestampMicrosecond, + tableFromIPC, + tableToIPC, + Utf8, + type Vector, + vectorFromArray, +} from "apache-arrow"; +import { describe, expect, test } from "vitest"; + +import { formatJsonArrayCell, parseJsonArrayCell } from "../json-array-cells"; + +function decimalVector(unscaled: bigint[], scale: number, precision: number) { + const words = new Uint32Array(unscaled.length * 4); + unscaled.forEach((value, i) => { + const bits = value < 0n ? (1n << 128n) + value : value; + for (let w = 0; w < 4; w++) { + words[i * 4 + w] = Number((bits >> BigInt(32 * w)) & 0xffffffffn); + } + }); + return makeVector( + makeData({ + type: new Decimal(scale, precision, 128), + length: unscaled.length, + data: words, + }), + ); +} + +const DAY = 86_400_000; + +const cases: Array<[string, () => Vector]> = [ + ["TINYINT", () => vectorFromArray([-128, -1, 0, 127], new Int8())], + ["SMALLINT", () => vectorFromArray([-32768, 32767], new Int16())], + ["INT", () => vectorFromArray([-2147483648, 0, 42, 2147483647], new Int32())], + [ + "BIGINT", + () => + vectorFromArray( + [-9223372036854775808n, 0n, 9007199254740993n, 9223372036854775807n], + new Int64(), + ), + ], + ["FLOAT", () => vectorFromArray([0, -0.25, 0.1, 3.4e38], new Float32())], + [ + "DOUBLE", + () => + vectorFromArray( + [0, 1.1, -1e-300, Number.MAX_VALUE, Number.NaN], + new Float64(), + ), + ], + [ + "DECIMAL(10,2)", + () => decimalVector([-50n, 0n, 51n, 5n, 9999999999n, -9999999999n], 2, 10), + ], + ["DECIMAL(38,0)", () => decimalVector([10n ** 37n, -1n], 0, 38)], + ["DECIMAL(5,5)", () => decimalVector([12345n, -1n], 5, 5)], + [ + "DATE", + () => + vectorFromArray( + [-DAY, Date.UTC(2024, 1, 29), Date.UTC(2026, 0, 1)], + new DateDay(), + ), + ], + [ + "TIMESTAMP", + () => + vectorFromArray( + [ + 0, + Date.UTC(2026, 0, 1, 10, 0, 0, 123), + Date.UTC(1969, 11, 31, 23, 59, 59, 999), + ], + new TimestampMicrosecond("UTC"), + ), + ], + [ + "TIMESTAMP_NTZ", + () => + vectorFromArray( + [0, Date.UTC(2026, 0, 1, 10, 0, 0, 123)], + new TimestampMicrosecond(), + ), + ], + ["BOOLEAN", () => vectorFromArray([true, false], new Bool())], + [ + "STRING", + () => vectorFromArray(["", "a", "ünïcödé ✓", '{"x": 1}'], new Utf8()), + ], + [ + "BINARY", + () => + vectorFromArray( + [new Uint8Array([]), new Uint8Array([0, 255, 1])], + new Binary(), + ), + ], +]; + +/** Values as an Arrow consumer reads them, comparable across decimal representations. */ +const comparable = (value: unknown) => + value instanceof Uint8Array + ? [...value] + : typeof value === "object" || typeof value === "bigint" + ? String(value) + : value; + +describe("JSON_ARRAY cell codecs", () => { + test.each(cases)( + "%s: parsing the formatted text gives back the native Arrow value", + (_name, build) => { + const column = tableFromIPC( + tableToIPC(new Table({ c: build() }), "stream"), + ).getChildAt(0); + const type = column?.type as DataType; + for (const native of column ?? []) { + const parsed = parseJsonArrayCell( + formatJsonArrayCell(native, type), + type, + ); + expect(comparable(parsed)).toEqual(comparable(native)); + } + }, + ); + + test("formats match what a warehouse writes", () => { + const read = (vector: Vector) => + tableFromIPC(tableToIPC(new Table({ c: vector }), "stream")).getChildAt( + 0, + ); + const decimal = read(decimalVector([-50n, 51n], 2, 10)); + const ntz = read( + vectorFromArray([Date.UTC(2026, 0, 1, 10)], new TimestampMicrosecond()), + ); + const day = read(vectorFromArray([Date.UTC(2026, 0, 1)], new DateDay())); + expect( + [...(decimal ?? [])].map((v) => + formatJsonArrayCell(v, decimal?.type as DataType), + ), + ).toEqual(["-0.50", "0.51"]); + expect(formatJsonArrayCell(ntz?.get(0), ntz?.type as DataType)).toBe( + "2026-01-01T10:00:00.000", + ); + expect(formatJsonArrayCell(day?.get(0), day?.type as DataType)).toBe( + "2026-01-01", + ); + }); + + test("a zoned TIMESTAMP keeps its instant when the warehouse writes the session offset", () => { + const type = new TimestampMicrosecond("UTC"); + expect(parseJsonArrayCell("2026-01-01T10:00:00.000-08:00", type)).toBe( + Date.UTC(2026, 0, 1, 18), + ); + }); + + test.each<[string, DataType, string]>([ + ["INT with a fraction", new Int32(), "1.5"], + ["INT in exponent form", new Int32(), "1e3"], + ["INT with padding", new Int32(), " 1"], + ["INT out of range", new Int32(), "2147483648"], + ["TINYINT out of range", new Int8(), "128"], + ["empty BIGINT", new Int64(), ""], + ["BIGINT with a fraction", new Int64(), "1.0"], + ["BIGINT with a plus sign", new Int64(), "+1"], + ["BIGINT with a leading zero", new Int64(), "01"], + ["empty DOUBLE", new Float64(), ""], + ["DOUBLE that is not a number", new Float64(), "abc"], + [ + "DECIMAL with more digits than its scale", + new Decimal(2, 10, 128), + "1.234", + ], + ["DECIMAL with two points", new Decimal(2, 10, 128), "1.2.3"], + ["DECIMAL with a plus sign", new Decimal(2, 10, 128), "+1.00"], + ["DATE without zero padding", new DateDay(), "2026-1-1"], + ["DATE with a time", new DateDay(), "2026-01-01T00:00:00Z"], + [ + "zoned TIMESTAMP without an offset", + new TimestampMicrosecond("UTC"), + "2026-01-01T10:00:00.000", + ], + [ + "zoned TIMESTAMP that is not a date", + new TimestampMicrosecond("UTC"), + "2026-13-01T10:00:00Z", + ], + [ + "TIMESTAMP_NTZ with Z", + new TimestampMicrosecond(), + "2026-01-01T10:00:00.000Z", + ], + [ + "TIMESTAMP_NTZ with an offset", + new TimestampMicrosecond(), + "2026-01-01T10:00:00.000-08:00", + ], + ["BOOLEAN in upper case", new Bool(), "TRUE"], + ["BOOLEAN as a digit", new Bool(), "1"], + ])("rejects %s", (_name, type, cell) => { + expect(() => parseJsonArrayCell(cell, type)).toThrow(/Invalid/); + }); +}); diff --git a/packages/appkit/src/plugins/analytics/result-delivery.ts b/packages/appkit/src/plugins/analytics/result-delivery.ts index e955b6211..b73bc7c3d 100644 --- a/packages/appkit/src/plugins/analytics/result-delivery.ts +++ b/packages/appkit/src/plugins/analytics/result-delivery.ts @@ -1,18 +1,16 @@ import { type DataType, type Field, Type, tableFromIPC } from "apache-arrow"; import type { SQLTypeMarker } from "shared"; -import { - type ArrowRowColumn, - buildArrowIPCFromRows, -} from "../../connectors/sql-warehouse/arrow-schema"; +import { buildArrowIPCFromRows } from "../../connectors/sql-warehouse/arrow-schema"; import { MAX_INLINE_ATTACHMENT_BYTES } from "../../connectors/sql-warehouse/client"; +import { formatJsonArrayCell } from "../../connectors/sql-warehouse/json-array-cells"; import { ExecutionError } from "../../errors"; import { createLogger } from "../../logging/logger"; import { EXTERNAL_LINK_DOWNLOAD_FAILED, type RefreshChunkLink, } from "../../stream/arrow-stream-processor"; -import type { ExternalLink } from "../../workspace-client"; +import type { ColumnInfo, ExternalLink } from "../../workspace-client"; /** * Centralized disposition/format fallback for analytics result delivery. @@ -53,7 +51,7 @@ export interface QueryExecutor { attachment?: string; data?: Record[]; dataArray?: Array>; - columns?: ArrowRowColumn[]; + columns?: ColumnInfo[]; external_links?: ExternalLink[]; columnNames?: string[]; totalByteCount?: number; @@ -575,70 +573,11 @@ export async function deliverJsonResult( const JSON_ARRAY_FALLBACK_MAX_ROWS = 100_000; const JSON_ARRAY_FALLBACK_MAX_BYTES = 64 * 1024 * 1024; -/** Render an apache-arrow Decimal cell to a fixed-point string. */ -function formatDecimalCell( - value: { toString(): string }, - scale: number, -): string { - const unscaled = value.toString(); - if (scale <= 0) return unscaled; - const negative = unscaled.startsWith("-"); - let digits = negative ? unscaled.slice(1) : unscaled; - if (digits.length <= scale) digits = digits.padStart(scale + 1, "0"); - const point = digits.length - scale; - const out = `${digits.slice(0, point)}.${digits.slice(point)}`; - return negative ? `-${out}` : out; -} - -/** - * Render an apache-arrow Timestamp cell to ISO-8601 ms precision — `Z` for - * zoned columns, no `Z` for TIMESTAMP_NTZ — matching native JSON_ARRAY. - */ -function formatTimestampCell(epochMs: number, hasTimezone: boolean): string { - const iso = new Date(epochMs).toISOString(); - return hasTimezone ? iso : iso.slice(0, -1); -} - -/** Render an apache-arrow Date cell to `yyyy-MM-dd` (UTC). */ -function formatDateCell(epochMs: number): string { - return new Date(epochMs).toISOString().slice(0, 10); -} - -/** - * Render a single scalar Arrow value to the string form the warehouse emits - * under native JSON_ARRAY. Used for both top-level cells and (recursively, via - * {@link renderNestedValue}) scalar leaves inside List/Struct/Map columns, - * keyed on the Arrow field type so a nested `DECIMAL` keeps its scale and a - * nested `TIMESTAMP` becomes ISO-8601 instead of raw epoch-ms. - */ -function formatScalarCell(value: unknown, type: DataType): string { - switch (type.typeId) { - case Type.Decimal: - return formatDecimalCell( - value as { toString(): string }, - (type as DataType & { scale: number }).scale, - ); - case Type.Timestamp: - return formatTimestampCell( - Number(value), - (type as DataType & { timezone?: string | null }).timezone != null, - ); - case Type.Date: - return formatDateCell(Number(value)); - default: - if (value instanceof Uint8Array) { - return Buffer.from(value).toString("base64"); - } - if (value instanceof Date) return value.toISOString(); - return String(value); - } -} - /** * Recursively render a nested Arrow value (List / Struct / Map, or a scalar * leaf) to the plain JS shape the warehouse nests inside a JSON_ARRAY cell: * structs and maps become objects, lists become arrays, and every scalar leaf - * is stringified per {@link formatScalarCell}. The caller `JSON.stringify`s the + * is stringified per {@link formatJsonArrayCell}. The caller `JSON.stringify`s the * result so nested columns match the warehouse's native `data_array` exactly. */ function renderNestedValue(value: unknown, type: DataType): unknown { @@ -678,7 +617,7 @@ function renderNestedValue(value: unknown, type: DataType): unknown { return obj; } default: - return formatScalarCell(value, type); + return formatJsonArrayCell(value, type); } } @@ -745,7 +684,7 @@ export function decodeArrowAttachmentToRows( case Type.Decimal: case Type.Timestamp: case Type.Date: - row[name] = formatScalarCell(v, type); + row[name] = formatJsonArrayCell(v, type); continue; // Nested columns (STRUCT / ARRAY / MAP) arrive on the native // JSON_ARRAY wire as a JSON string with every scalar leaf stringified @@ -769,7 +708,7 @@ export function decodeArrowAttachmentToRows( } else if (v instanceof Uint8Array) { row[name] = Buffer.from(v).toString("base64"); } else { - row[name] = formatScalarCell(v, type); + row[name] = formatJsonArrayCell(v, type); } } rows.push(row); diff --git a/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts index d6431a1c9..233f487b1 100644 --- a/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts +++ b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts @@ -83,14 +83,14 @@ export function typedDataset(rows = 20): Dataset { const flag = col(7, (i) => i % 2 === 0); const table = new ArrowTable({ - col_0: vectorFromArray(id, new Int32()), - col_1: vectorFromArray(big, new Int64()), - col_2: vectorFromArray(amount, new Float64()), - col_3: decimalVector(price, 2), - col_4: vectorFromArray(day, new DateDay()), - col_5: vectorFromArray(ts, new TimestampMicrosecond("UTC")), - col_6: vectorFromArray(label, new Utf8()), - col_7: vectorFromArray(flag, new Bool()), + id: vectorFromArray(id, new Int32()), + big: vectorFromArray(big, new Int64()), + amount: vectorFromArray(amount, new Float64()), + price: decimalVector(price, 2), + day: vectorFromArray(day, new DateDay()), + ts: vectorFromArray(ts, new TimestampMicrosecond("UTC")), + label: vectorFromArray(label, new Utf8()), + flag: vectorFromArray(flag, new Bool()), }); const text = (v: unknown, render: (x: never) => string) => @@ -134,8 +134,8 @@ export function typedDataset(rows = 20): Dataset { /** Every cell as a string, so schemas and values compare across Arrow builds. */ export function cellsOf(table: Table): Array> { - const columns = table.schema.fields.map((f) => - [...(table.getChild(f.name) as Iterable)].map((v) => + const columns = table.schema.fields.map((_f, index) => + [...(table.getChildAt(index) as Iterable)].map((v) => v === null || v === undefined ? null : String(v), ), ); From ce8e5cc205b6aea69577659625253c4d81419b04 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 10 Nov 2026 09:44:18 +0000 Subject: [PATCH 5/9] fix(deps): keep the template off npm versions the security policy blocks The template lockfiles from the v0.91.2 sync pin versions that Databricks' npm proxies refuse with 403: import-in-the-middle 3.6.0, semantic-conventions 1.45.0, ansi-regex 6.6.0, and the OpenTelemetry 0.223.1 / 2.12.1 patch release. Every Template Deploy-Shape and PR Template Artifact run failed on install. Pin import-in-the-middle to 3.5.3 and semantic-conventions to 1.44.0 under the bunyan and pino instrumentations, which need ^1.41.1. Re-resolve the other blocked npm lock entries, which lands on the allowed 0.223.0 / 2.12.0 and ansi-regex 6.5.0. Signed-off-by: Pedro Figueiredo --- template/package-lock.json | 1457 +++++----------------------------- template/package.json | 9 +- template/pnpm-lock.yaml | 12 +- template/pnpm-workspace.yaml | 2 + 4 files changed, 220 insertions(+), 1260 deletions(-) diff --git a/template/package-lock.json b/template/package-lock.json index 83e567461..5cb855409 100644 --- a/template/package-lock.json +++ b/template/package-lock.json @@ -2246,709 +2246,92 @@ } }, "node_modules/@opentelemetry/configuration": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.223.1.tgz", - "integrity": "sha512-+u+83OR4tG3KxG7PV+Sh8HJ+OnaOJpjs5D3DxxOS7ovgR7ulzt7ZXyBJYLQJEfsuHZJxzW3M2yG4WcyPKKCexQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "yaml": "^2.8.3" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.12.1.tgz", - "integrity": "sha512-1+zgZyD7QvBr0/K6+FVx5UAzdGo0shnQf8pbWUFFbj43qZPIKTtfK4xX4J3bS7AmkifiZzAEMdk8dztiEfkgmg==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz", - "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.223.1.tgz", - "integrity": "sha512-aMOXQV9Xx90EmqoqMj7KTlOtHnFB/iYyAaHdId/CNWXV5bH1wSmPj/i9nQxg8w1lLo+CI1cSRcuUD7DAPFrOVQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-grpc-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1", - "@opentelemetry/sdk-logs": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.1.tgz", - "integrity": "sha512-DtIY6YRp0Q0GRVtiKH+N6zAoXQzYtlwbXqZEN/UhBgkKgHYPo2RF6vvB8jGebwcESwquS0S3Vn5fouu7dI17Tg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.1.tgz", - "integrity": "sha512-TVbeqdBiYKS12OWZOk3y709l0yh66mCSAO8bSbIK8/Og3MQmy1vVBWEp+HbJvONmc02c226/8t2r7hmEgGcMvA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-transformer": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.223.1.tgz", - "integrity": "sha512-22EJNk0qjI3klrON0aFR0BXye6FOM2vrkpjXjW8KDeSSiMez7+T4ARXfAh8+ucS3dEnhpSoGdfs4E/z4EIYjXQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-logs": "0.223.1", - "@opentelemetry/sdk-metrics": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.1.tgz", - "integrity": "sha512-IwrNcuLCvnJAnNLkXxx0Uw+rjBkVW6VGQrhd7PanNf0LavOzkTk9sDAjdqegM7p/VdxefNRrii6d+wBRJfz35g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.223.1.tgz", - "integrity": "sha512-GpL453eVjCUn2fC3LYBuckCyPleN7FqpvmndT2kZ8gO8c6hHRL5X1BqLLDbVmdw1QDT4ZVyK9G4MNfz5xEkXew==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.1.tgz", - "integrity": "sha512-HeSblYhpds4QWliBE7ANCfoa2p3QH83uUxX4vO2qtk6BhIcqP7qDYYT36z3LOXftEBpdfbDStZxv+Cw5mqKX5g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-trace": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.1.tgz", - "integrity": "sha512-vANdmCYnn+Ka/alBKxwICMl4Koa+aQBlRC9zAZy+Ig1Obbo3RZt2oiyv+8/gET5rrBBYrO3YniPcXfwZrN01rg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.223.1.tgz", - "integrity": "sha512-8YEUwMllir4SyAOmFb3NjcLJqBdUNqaRwTDZ4+yegnWnTAzMBbJzd06ZTewhOstPJUblPwqHuwpXM8aSHIXtTw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1", - "@opentelemetry/sdk-logs": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/api-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.1.tgz", - "integrity": "sha512-DtIY6YRp0Q0GRVtiKH+N6zAoXQzYtlwbXqZEN/UhBgkKgHYPo2RF6vvB8jGebwcESwquS0S3Vn5fouu7dI17Tg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.1.tgz", - "integrity": "sha512-TVbeqdBiYKS12OWZOk3y709l0yh66mCSAO8bSbIK8/Og3MQmy1vVBWEp+HbJvONmc02c226/8t2r7hmEgGcMvA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-transformer": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.223.1.tgz", - "integrity": "sha512-22EJNk0qjI3klrON0aFR0BXye6FOM2vrkpjXjW8KDeSSiMez7+T4ARXfAh8+ucS3dEnhpSoGdfs4E/z4EIYjXQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-logs": "0.223.1", - "@opentelemetry/sdk-metrics": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.1.tgz", - "integrity": "sha512-IwrNcuLCvnJAnNLkXxx0Uw+rjBkVW6VGQrhd7PanNf0LavOzkTk9sDAjdqegM7p/VdxefNRrii6d+wBRJfz35g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.223.1.tgz", - "integrity": "sha512-GpL453eVjCUn2fC3LYBuckCyPleN7FqpvmndT2kZ8gO8c6hHRL5X1BqLLDbVmdw1QDT4ZVyK9G4MNfz5xEkXew==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.1.tgz", - "integrity": "sha512-HeSblYhpds4QWliBE7ANCfoa2p3QH83uUxX4vO2qtk6BhIcqP7qDYYT36z3LOXftEBpdfbDStZxv+Cw5mqKX5g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-trace": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.1.tgz", - "integrity": "sha512-vANdmCYnn+Ka/alBKxwICMl4Koa+aQBlRC9zAZy+Ig1Obbo3RZt2oiyv+8/gET5rrBBYrO3YniPcXfwZrN01rg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.223.0.tgz", - "integrity": "sha512-wt33CkhmLfFDSRXNOABWQzVfks1PQvsRNIHqiY5Y5Hi6SiVF7iJYDTqpaUhbwcmyJYcB0LzBNvsic8GyufMxAA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.223.0", - "@opentelemetry/otlp-transformer": "0.223.0", - "@opentelemetry/sdk-logs": "0.223.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.223.1.tgz", - "integrity": "sha512-GlY6JxBGJ7VnxxvO/CF7B5aSugRvJxDI6EzEMy42t0BLOPui3MChuyUMNfMnUXm9Q2/ufc0tZnw5GLAgsP9oVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/exporter-metrics-otlp-http": "0.223.1", - "@opentelemetry/otlp-grpc-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.1.tgz", - "integrity": "sha512-DtIY6YRp0Q0GRVtiKH+N6zAoXQzYtlwbXqZEN/UhBgkKgHYPo2RF6vvB8jGebwcESwquS0S3Vn5fouu7dI17Tg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.223.1.tgz", - "integrity": "sha512-ZJrshM+HZ9R02g5BZ7vCVLYs9nfgQIcm4dNtpvHCwJubX9V0k2nYc/VfgNW/3GiWFIPWXh0K+80ZcfUh5H1r3w==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-metrics": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.1.tgz", - "integrity": "sha512-TVbeqdBiYKS12OWZOk3y709l0yh66mCSAO8bSbIK8/Og3MQmy1vVBWEp+HbJvONmc02c226/8t2r7hmEgGcMvA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-transformer": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.223.1.tgz", - "integrity": "sha512-22EJNk0qjI3klrON0aFR0BXye6FOM2vrkpjXjW8KDeSSiMez7+T4ARXfAh8+ucS3dEnhpSoGdfs4E/z4EIYjXQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-logs": "0.223.1", - "@opentelemetry/sdk-metrics": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.1.tgz", - "integrity": "sha512-IwrNcuLCvnJAnNLkXxx0Uw+rjBkVW6VGQrhd7PanNf0LavOzkTk9sDAjdqegM7p/VdxefNRrii6d+wBRJfz35g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.223.1.tgz", - "integrity": "sha512-GpL453eVjCUn2fC3LYBuckCyPleN7FqpvmndT2kZ8gO8c6hHRL5X1BqLLDbVmdw1QDT4ZVyK9G4MNfz5xEkXew==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.1.tgz", - "integrity": "sha512-HeSblYhpds4QWliBE7ANCfoa2p3QH83uUxX4vO2qtk6BhIcqP7qDYYT36z3LOXftEBpdfbDStZxv+Cw5mqKX5g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-trace": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.1.tgz", - "integrity": "sha512-vANdmCYnn+Ka/alBKxwICMl4Koa+aQBlRC9zAZy+Ig1Obbo3RZt2oiyv+8/gET5rrBBYrO3YniPcXfwZrN01rg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.223.0.tgz", - "integrity": "sha512-hNC8yai/+xCNyTuiOy6JFepXXA/sb/2jCzQRY7c2cUWz6owaidihlHUpO20B9/dj8JKPNo8rcn7ikTy7Cj+JaQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.0", - "@opentelemetry/otlp-exporter-base": "0.223.0", - "@opentelemetry/otlp-transformer": "0.223.0", - "@opentelemetry/resources": "2.12.0", - "@opentelemetry/sdk-metrics": "2.12.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.223.0.tgz", - "integrity": "sha512-DlwYBlqUVlAQqGXOES1K92b6FyHwB2phwcsccXEQhE9Mz1CAEymQgyRwbPKjCbpySZE1ASy2s9t2kIQ1pU/tmg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/exporter-metrics-otlp-http": "0.223.0", - "@opentelemetry/otlp-exporter-base": "0.223.0", - "@opentelemetry/otlp-transformer": "0.223.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.223.1.tgz", - "integrity": "sha512-kE64r4dboDUscC01kP6T/v62sLX2gQ5i9K0I8E5tMj7oTjh9vqzsBof/77Xf59kaihiQS3lZdN3N8kii75pbhg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-metrics": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.1.tgz", - "integrity": "sha512-IwrNcuLCvnJAnNLkXxx0Uw+rjBkVW6VGQrhd7PanNf0LavOzkTk9sDAjdqegM7p/VdxefNRrii6d+wBRJfz35g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.1.tgz", - "integrity": "sha512-HeSblYhpds4QWliBE7ANCfoa2p3QH83uUxX4vO2qtk6BhIcqP7qDYYT36z3LOXftEBpdfbDStZxv+Cw5mqKX5g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.223.1.tgz", - "integrity": "sha512-rgfvGgWixg/hk+0S4CtWIxR9NIHyKWbzoO6fikOtFn3ir7Btk2zTf9ycrqvInVRVkv+di+W2CedcKDmaXeLpnQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-grpc-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1", - "@opentelemetry/sdk-trace": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.1.tgz", - "integrity": "sha512-DtIY6YRp0Q0GRVtiKH+N6zAoXQzYtlwbXqZEN/UhBgkKgHYPo2RF6vvB8jGebwcESwquS0S3Vn5fouu7dI17Tg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.1.tgz", - "integrity": "sha512-TVbeqdBiYKS12OWZOk3y709l0yh66mCSAO8bSbIK8/Og3MQmy1vVBWEp+HbJvONmc02c226/8t2r7hmEgGcMvA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-transformer": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.223.1.tgz", - "integrity": "sha512-22EJNk0qjI3klrON0aFR0BXye6FOM2vrkpjXjW8KDeSSiMez7+T4ARXfAh8+ucS3dEnhpSoGdfs4E/z4EIYjXQ==", + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.223.0.tgz", + "integrity": "sha512-KgyHXS8weyh+mJWVZ2+3hmu7J4mwU661Y8yQ1PvBAEY7G63X8qfGr6uN61MNJrN+YzP+dC5x6PmBEgdh7x6coQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-logs": "0.223.1", - "@opentelemetry/sdk-metrics": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1" + "@opentelemetry/core": "2.12.0", + "yaml": "^2.8.3" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": "^1.9.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.1.tgz", - "integrity": "sha512-IwrNcuLCvnJAnNLkXxx0Uw+rjBkVW6VGQrhd7PanNf0LavOzkTk9sDAjdqegM7p/VdxefNRrii6d+wBRJfz35g==", + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.12.0.tgz", + "integrity": "sha512-iK/1nD7mmZVkuYjVjkdFBiCvzntJdFny1S3CYbUYyOq7V5enL6XX+gJ7eh965L1LnS11VA8mQ7Ej/ck04SB0MQ==", "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.223.1.tgz", - "integrity": "sha512-GpL453eVjCUn2fC3LYBuckCyPleN7FqpvmndT2kZ8gO8c6hHRL5X1BqLLDbVmdw1QDT4ZVyK9G4MNfz5xEkXew==", + "node_modules/@opentelemetry/core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz", + "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.1.tgz", - "integrity": "sha512-HeSblYhpds4QWliBE7ANCfoa2p3QH83uUxX4vO2qtk6BhIcqP7qDYYT36z3LOXftEBpdfbDStZxv+Cw5mqKX5g==", + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.223.0.tgz", + "integrity": "sha512-O/ztY4A+NRi9bQm9Wlj9aCh2s1lbnkA8tcv+88crBMqas6YK8AnPvKXSms3Mn1nto4ylAIQAAxjtbzCMZjGvwg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1" + "@opentelemetry/otlp-exporter-base": "0.223.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.223.0", + "@opentelemetry/otlp-transformer": "0.223.0", + "@opentelemetry/sdk-logs": "0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.1.tgz", - "integrity": "sha512-vANdmCYnn+Ka/alBKxwICMl4Koa+aQBlRC9zAZy+Ig1Obbo3RZt2oiyv+8/gET5rrBBYrO3YniPcXfwZrN01rg==", + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.223.0.tgz", + "integrity": "sha512-YQh9+t/JmV9OeRkoLhPb4yvcrWCX6lAOmXFos/JKoD+KITZYZoxf0acnwAk6YDI9O7fX68O2ka38hx1G9exl8w==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/otlp-exporter-base": "0.223.0", + "@opentelemetry/otlp-transformer": "0.223.0", + "@opentelemetry/sdk-logs": "0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.223.1.tgz", - "integrity": "sha512-GFUExKrz836eYHXMak4ucjLl9EC5q4CgWwzCcj8hSGltLOTpkhbE24Y/7jc3DlygcTQzVVphv6STvnfKPasH/g==", + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.223.0.tgz", + "integrity": "sha512-wt33CkhmLfFDSRXNOABWQzVfks1PQvsRNIHqiY5Y5Hi6SiVF7iJYDTqpaUhbwcmyJYcB0LzBNvsic8GyufMxAA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1", - "@opentelemetry/sdk-trace": "2.12.1" + "@opentelemetry/otlp-exporter-base": "0.223.0", + "@opentelemetry/otlp-transformer": "0.223.0", + "@opentelemetry/sdk-logs": "0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2957,26 +2340,15 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/api-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.1.tgz", - "integrity": "sha512-DtIY6YRp0Q0GRVtiKH+N6zAoXQzYtlwbXqZEN/UhBgkKgHYPo2RF6vvB8jGebwcESwquS0S3Vn5fouu7dI17Tg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.1.tgz", - "integrity": "sha512-TVbeqdBiYKS12OWZOk3y709l0yh66mCSAO8bSbIK8/Og3MQmy1vVBWEp+HbJvONmc02c226/8t2r7hmEgGcMvA==", + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.223.0.tgz", + "integrity": "sha512-QneYDf2PWYAqmikS1/jeG8xCv0Q2EOSBm0RRM7IFU9zEnKLu7qxdlsz9t3YjxiAo7XzqfjTsJZys5qhYZAQ2PQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-transformer": "0.223.1" + "@opentelemetry/exporter-metrics-otlp-http": "0.223.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.223.0", + "@opentelemetry/otlp-transformer": "0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2985,18 +2357,17 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.223.1.tgz", - "integrity": "sha512-22EJNk0qjI3klrON0aFR0BXye6FOM2vrkpjXjW8KDeSSiMez7+T4ARXfAh8+ucS3dEnhpSoGdfs4E/z4EIYjXQ==", + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.223.0.tgz", + "integrity": "sha512-hNC8yai/+xCNyTuiOy6JFepXXA/sb/2jCzQRY7c2cUWz6owaidihlHUpO20B9/dj8JKPNo8rcn7ikTy7Cj+JaQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-logs": "0.223.1", - "@opentelemetry/sdk-metrics": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1" + "@opentelemetry/core": "2.12.0", + "@opentelemetry/otlp-exporter-base": "0.223.0", + "@opentelemetry/otlp-transformer": "0.223.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/sdk-metrics": "2.12.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3005,71 +2376,74 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.1.tgz", - "integrity": "sha512-IwrNcuLCvnJAnNLkXxx0Uw+rjBkVW6VGQrhd7PanNf0LavOzkTk9sDAjdqegM7p/VdxefNRrii6d+wBRJfz35g==", + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.223.0.tgz", + "integrity": "sha512-DlwYBlqUVlAQqGXOES1K92b6FyHwB2phwcsccXEQhE9Mz1CAEymQgyRwbPKjCbpySZE1ASy2s9t2kIQ1pU/tmg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/exporter-metrics-otlp-http": "0.223.0", + "@opentelemetry/otlp-exporter-base": "0.223.0", + "@opentelemetry/otlp-transformer": "0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.223.1.tgz", - "integrity": "sha512-GpL453eVjCUn2fC3LYBuckCyPleN7FqpvmndT2kZ8gO8c6hHRL5X1BqLLDbVmdw1QDT4ZVyK9G4MNfz5xEkXew==", + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.223.0.tgz", + "integrity": "sha512-HhbN3/B8vG+JNKH2oLMynFFrz2O/o+WFklfwiVq3ffdRWOEZx1zHeYe6r86fn9BUSpQ2U5cXfQja+otTdzi5qQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", + "@opentelemetry/core": "2.12.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/sdk-metrics": "2.12.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.1.tgz", - "integrity": "sha512-HeSblYhpds4QWliBE7ANCfoa2p3QH83uUxX4vO2qtk6BhIcqP7qDYYT36z3LOXftEBpdfbDStZxv+Cw5mqKX5g==", + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.223.0.tgz", + "integrity": "sha512-gYDt98a44cvdbJNL+fFCFO7k7qSGaWGZAb6WUXGCKL0RiRhF96N6jlH4wXMzS1daZoV5AMHZZixjaLpQBMhpvA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1" + "@opentelemetry/otlp-exporter-base": "0.223.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.223.0", + "@opentelemetry/otlp-transformer": "0.223.0", + "@opentelemetry/sdk-trace": "2.12.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.1.tgz", - "integrity": "sha512-vANdmCYnn+Ka/alBKxwICMl4Koa+aQBlRC9zAZy+Ig1Obbo3RZt2oiyv+8/gET5rrBBYrO3YniPcXfwZrN01rg==", + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.223.0.tgz", + "integrity": "sha512-N8NWZaoQuF9e307S6xoVoI1lvxOOFkmBnynD42r02CPCfxoC95cJExAKBFrTD4C5iuhqthbzOZJN/W29ez4o/A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/otlp-exporter-base": "0.223.0", + "@opentelemetry/otlp-transformer": "0.223.0", + "@opentelemetry/sdk-trace": "2.12.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { @@ -3090,14 +2464,14 @@ } }, "node_modules/@opentelemetry/exporter-zipkin": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.12.1.tgz", - "integrity": "sha512-OVYgxxuOk6Gi++PZVCUO4Nspg6ElT+pBToorq7coxLgkKRaBzQ2va/z1dOY/21tufwvP6ID5TSCxmCFW2HLkiA==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.12.0.tgz", + "integrity": "sha512-XfHO6VxPkVD/QaXqgppdHK2DTqmtHLH06ggslhm5hVYAS+dwD3okaeIyhxXBbhmWn9HhCYcv285eK1TgyF0wYQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1", + "@opentelemetry/core": "2.12.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/sdk-trace": "2.12.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3107,39 +2481,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/resources": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.1.tgz", - "integrity": "sha512-IwrNcuLCvnJAnNLkXxx0Uw+rjBkVW6VGQrhd7PanNf0LavOzkTk9sDAjdqegM7p/VdxefNRrii6d+wBRJfz35g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/sdk-trace": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.1.tgz", - "integrity": "sha512-vANdmCYnn+Ka/alBKxwICMl4Koa+aQBlRC9zAZy+Ig1Obbo3RZt2oiyv+8/gET5rrBBYrO3YniPcXfwZrN01rg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/instrumentation": { "version": "0.223.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.223.0.tgz", @@ -3227,9 +2568,9 @@ } }, "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.45.0.tgz", - "integrity": "sha512-aOP45mbcOKbQVvFWfQmgoOPT8R2iwbbbgl83k4CujGV9BrJhFr3qG55MFpwFs4YLabpuW3HhzUY9eQxPlDqaZw==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.44.0.tgz", + "integrity": "sha512-E3JkOlMBi3N8OGGkbl7tFyqk2MAS6/lySRZBK3Mu7AzjF1fT8oIChH9ChE48SUWzDn9Kdwta+G/p5LJ4xW4CLQ==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -3379,12 +2720,12 @@ } }, "node_modules/@opentelemetry/instrumentation-grpc": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.223.1.tgz", - "integrity": "sha512-PYFqHpOIoXqfru0TPjZPmQUZim1tqGlMFSbsa4AUx2Gw1GlBRk7q3hwo7syrUytZaF5isQdXObAzmO/kUoDcfw==", + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.223.0.tgz", + "integrity": "sha512-G/E2zD9S8MTOPVtswQIC39BVWw47bTDD7Gg0FcfJtVw06lSwpSdUQMJZ74sJoJUycu7L/VsFJpNnqiC5h0X1VQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "0.223.1", + "@opentelemetry/instrumentation": "0.223.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -3394,35 +2735,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.1.tgz", - "integrity": "sha512-DtIY6YRp0Q0GRVtiKH+N6zAoXQzYtlwbXqZEN/UhBgkKgHYPo2RF6vvB8jGebwcESwquS0S3Vn5fouu7dI17Tg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-grpc/node_modules/@opentelemetry/instrumentation": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.223.1.tgz", - "integrity": "sha512-e1qb4yd2kfxc+TG/p7fEk0Ja9a/t8ZHXiEsuE67Ba16hlHXvXHMP9Wh2AZMp6IQ5oqC2rOFE1hgvJXg3VuF/fg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/instrumentation-hapi": { "version": "0.69.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.69.0.tgz", @@ -3743,9 +3055,9 @@ } }, "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.45.0.tgz", - "integrity": "sha512-aOP45mbcOKbQVvFWfQmgoOPT8R2iwbbbgl83k4CujGV9BrJhFr3qG55MFpwFs4YLabpuW3HhzUY9eQxPlDqaZw==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.44.0.tgz", + "integrity": "sha512-E3JkOlMBi3N8OGGkbl7tFyqk2MAS6/lySRZBK3Mu7AzjF1fT8oIChH9ChE48SUWzDn9Kdwta+G/p5LJ4xW4CLQ==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -3809,87 +3121,7 @@ "dependencies": { "@opentelemetry/api-logs": "^0.223.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.223.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-socket.io": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.70.0.tgz", - "integrity": "sha512-7Xei0+AEZly3Kqvv3kMYlhqVRzcBcR7Cw/YHkV8pV8Xc5LeuVz5TUaJoiZLsLEN9ffpSYhh+BVUMDgKX84xIhw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.223.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.42.0.tgz", - "integrity": "sha512-qATqe/ZNH/qNEFKDyJhR0kw1hcBt5ftTlkPgdjOcUVkQKAe79LO6lhJrSziTwNSxjXUNp0PjF5AIGAlafN/G+A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.223.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.33.0.tgz", - "integrity": "sha512-eR4Y9kApgnKmiIoygMtyfABTL4bwAUlt00/Nxkkr5FSC28kcM6MxzTp5mNw+KD1XcVt6dBj3Sz5aoekbTqY9GQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.9.0", - "@opentelemetry/instrumentation": "^0.223.0", - "@opentelemetry/semantic-conventions": "^1.24.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.7.0" - } - }, - "node_modules/@opentelemetry/instrumentation-winston": { - "version": "0.68.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.68.1.tgz", - "integrity": "sha512-J+ko44pKaIqDF0xS5YbwHU0DlcrX6LTC3tjmJZdJNKiYkZ/xe7bI82Plk2VdNk8Ns4NfIPVUrMaYNQ6GySfTnw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "^0.223.0", - "@opentelemetry/instrumentation": "^0.223.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.223.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.0.tgz", - "integrity": "sha512-HlJMrJXjOqdQeoYNDHEvFTpaY2p9uWDK1GPl5AZ4wI5easzfoppTduOv47M8eiF+4ew42vfI/Xvt7THYgESnRw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.0", - "@opentelemetry/otlp-transformer": "0.223.0" + "@opentelemetry/instrumentation": "^0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3898,16 +3130,13 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.223.1.tgz", - "integrity": "sha512-JCkwwI7YHy62CTV3v5K5Zra7/2tKOXOVrihcuflkFOm2DUoZwwohmiTAnrfcMTdo42QI0RSmYTMwRFSwsVpB7g==", + "node_modules/@opentelemetry/instrumentation-socket.io": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.70.0.tgz", + "integrity": "sha512-7Xei0+AEZly3Kqvv3kMYlhqVRzcBcR7Cw/YHkV8pV8Xc5LeuVz5TUaJoiZLsLEN9ffpSYhh+BVUMDgKX84xIhw==", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1" + "@opentelemetry/instrumentation": "^0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3916,26 +3145,14 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/api-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.1.tgz", - "integrity": "sha512-DtIY6YRp0Q0GRVtiKH+N6zAoXQzYtlwbXqZEN/UhBgkKgHYPo2RF6vvB8jGebwcESwquS0S3Vn5fouu7dI17Tg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.1.tgz", - "integrity": "sha512-TVbeqdBiYKS12OWZOk3y709l0yh66mCSAO8bSbIK8/Og3MQmy1vVBWEp+HbJvONmc02c226/8t2r7hmEgGcMvA==", + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.42.0.tgz", + "integrity": "sha512-qATqe/ZNH/qNEFKDyJhR0kw1hcBt5ftTlkPgdjOcUVkQKAe79LO6lhJrSziTwNSxjXUNp0PjF5AIGAlafN/G+A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-transformer": "0.223.1" + "@opentelemetry/instrumentation": "^0.223.0", + "@opentelemetry/semantic-conventions": "^1.33.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -3944,91 +3161,91 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.223.1.tgz", - "integrity": "sha512-22EJNk0qjI3klrON0aFR0BXye6FOM2vrkpjXjW8KDeSSiMez7+T4ARXfAh8+ucS3dEnhpSoGdfs4E/z4EIYjXQ==", + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.33.0.tgz", + "integrity": "sha512-eR4Y9kApgnKmiIoygMtyfABTL4bwAUlt00/Nxkkr5FSC28kcM6MxzTp5mNw+KD1XcVt6dBj3Sz5aoekbTqY9GQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-logs": "0.223.1", - "@opentelemetry/sdk-metrics": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1" + "@opentelemetry/core": "^2.9.0", + "@opentelemetry/instrumentation": "^0.223.0", + "@opentelemetry/semantic-conventions": "^1.24.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": "^1.7.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/resources": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.1.tgz", - "integrity": "sha512-IwrNcuLCvnJAnNLkXxx0Uw+rjBkVW6VGQrhd7PanNf0LavOzkTk9sDAjdqegM7p/VdxefNRrii6d+wBRJfz35g==", + "node_modules/@opentelemetry/instrumentation-winston": { + "version": "0.68.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.68.1.tgz", + "integrity": "sha512-J+ko44pKaIqDF0xS5YbwHU0DlcrX6LTC3tjmJZdJNKiYkZ/xe7bI82Plk2VdNk8Ns4NfIPVUrMaYNQ6GySfTnw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/api-logs": "^0.223.0", + "@opentelemetry/instrumentation": "^0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.223.1.tgz", - "integrity": "sha512-GpL453eVjCUn2fC3LYBuckCyPleN7FqpvmndT2kZ8gO8c6hHRL5X1BqLLDbVmdw1QDT4ZVyK9G4MNfz5xEkXew==", + "node_modules/@opentelemetry/instrumentation/node_modules/es-module-lexer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", + "license": "MIT" + }, + "node_modules/@opentelemetry/instrumentation/node_modules/import-in-the-middle": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.3.tgz", + "integrity": "sha512-usm/IiY63VtOyx/dm7uEoZV+EsanCDmwRUroQZIkFLGu1NVXZ8mZqIOhaN4QIJ7EsbMTuQbJXwCySCx+ozMA8g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.3", + "module-details-from-path": "^1.0.4" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" + "node": ">=18" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.1.tgz", - "integrity": "sha512-HeSblYhpds4QWliBE7ANCfoa2p3QH83uUxX4vO2qtk6BhIcqP7qDYYT36z3LOXftEBpdfbDStZxv+Cw5mqKX5g==", + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.0.tgz", + "integrity": "sha512-HlJMrJXjOqdQeoYNDHEvFTpaY2p9uWDK1GPl5AZ4wI5easzfoppTduOv47M8eiF+4ew42vfI/Xvt7THYgESnRw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1" + "@opentelemetry/core": "2.12.0", + "@opentelemetry/otlp-transformer": "0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-trace": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.1.tgz", - "integrity": "sha512-vANdmCYnn+Ka/alBKxwICMl4Koa+aQBlRC9zAZy+Ig1Obbo3RZt2oiyv+8/gET5rrBBYrO3YniPcXfwZrN01rg==", + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.223.0.tgz", + "integrity": "sha512-62DuT9qzwFEFPq1aguQ+P6cWC4sJPwMxCj8wZ3Q+mH8LpPcV6zdv5gj1YtGIGZF5FcKt9JnDVraey0CEnu9MtQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.12.0", + "@opentelemetry/otlp-exporter-base": "0.223.0", + "@opentelemetry/otlp-transformer": "0.223.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, "node_modules/@opentelemetry/otlp-transformer": { @@ -4064,12 +3281,12 @@ } }, "node_modules/@opentelemetry/propagator-b3": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.12.1.tgz", - "integrity": "sha512-CqVrFxnioX9BeQm6T2wk3tSIEwVM3Q3lwlO68R3MyZQOW+fDbuxAogB5XUmV4OJ9JiF3XzxP4aiKmP0SwFUrrw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.12.0.tgz", + "integrity": "sha512-ktio0dVz2Uo83+EESOqMwqdJqoZKcf4EtmuG0mxddMKGbwN2Tignyq7JR9R7SBr1riwevitke+tpYQTMS/Fi0g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1" + "@opentelemetry/core": "2.12.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4079,12 +3296,13 @@ } }, "node_modules/@opentelemetry/propagator-jaeger": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.12.1.tgz", - "integrity": "sha512-dLbaELQoQ3irP8FIVxB+7wkk+iqEKldEAVyg82yNG/F67pra74ud7dxPxIERBq5HBTyCxpfQn2a1UPdX4RAvjw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.12.0.tgz", + "integrity": "sha512-Kz2Vz+NILW7S1WtN4rV1wr76fMtT/RRWvAMFBc91uTIi2dDMXxrfBKNe2KH7yaCfFp1X0xfAuabzuFczvoPB6A==", + "deprecated": "Package no longer supported. See https://github.com/open-telemetry/opentelemetry-js/pull/7077", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1" + "@opentelemetry/core": "2.12.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4236,257 +3454,37 @@ } }, "node_modules/@opentelemetry/sdk-node": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.223.1.tgz", - "integrity": "sha512-6N4e/cuE742yHQICuSdiYc4ym6gSggT2cCs5bltgfGvQyUH/63XMlm4KLbPzAT3GuiHuSLXmiOBIYo9/H+tpNQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/configuration": "0.223.1", - "@opentelemetry/context-async-hooks": "2.12.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/exporter-logs-otlp-grpc": "0.223.1", - "@opentelemetry/exporter-logs-otlp-http": "0.223.1", - "@opentelemetry/exporter-logs-otlp-proto": "0.223.1", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.223.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.223.1", - "@opentelemetry/exporter-metrics-otlp-proto": "0.223.1", - "@opentelemetry/exporter-prometheus": "0.223.1", - "@opentelemetry/exporter-trace-otlp-grpc": "0.223.1", - "@opentelemetry/exporter-trace-otlp-http": "0.223.1", - "@opentelemetry/exporter-trace-otlp-proto": "0.223.1", - "@opentelemetry/exporter-zipkin": "2.12.1", - "@opentelemetry/instrumentation": "0.223.1", - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-grpc-exporter-base": "0.223.1", - "@opentelemetry/propagator-b3": "2.12.1", - "@opentelemetry/propagator-jaeger": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-logs": "0.223.1", - "@opentelemetry/sdk-metrics": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1", - "@opentelemetry/sdk-trace-base": "2.12.1", - "@opentelemetry/sdk-trace-node": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/api-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.1.tgz", - "integrity": "sha512-DtIY6YRp0Q0GRVtiKH+N6zAoXQzYtlwbXqZEN/UhBgkKgHYPo2RF6vvB8jGebwcESwquS0S3Vn5fouu7dI17Tg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.223.1.tgz", - "integrity": "sha512-YE2kBJUTestl3ggI8UgvhAoWRsW/0BBsjcka4Aj0pv+YXwOqF7IOSSlyE5kkX7zmEvF3FcfgknvMbGp0tAjKCg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1", - "@opentelemetry/sdk-logs": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.223.1.tgz", - "integrity": "sha512-ZJrshM+HZ9R02g5BZ7vCVLYs9nfgQIcm4dNtpvHCwJubX9V0k2nYc/VfgNW/3GiWFIPWXh0K+80ZcfUh5H1r3w==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-metrics": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.223.1.tgz", - "integrity": "sha512-jZC4opUQhCTtNDH6L6GAg1n1uhp7nNiDYb8x333t2/asR7OtWh/6IElUwXE0f/VVSgupmYhWXCtNp8I0EoccVA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/exporter-metrics-otlp-http": "0.223.1", - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.223.1.tgz", - "integrity": "sha512-VVzPl7gbROCt7at9bS6GCMp6Re/7Kf+ngHnZhrhZl8iMvvAdFzP6evz7aj2Yfq/pSg6XE0LLFF7SWXb9iTswiQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.223.1", - "@opentelemetry/otlp-transformer": "0.223.1", - "@opentelemetry/sdk-trace": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.223.1.tgz", - "integrity": "sha512-e1qb4yd2kfxc+TG/p7fEk0Ja9a/t8ZHXiEsuE67Ba16hlHXvXHMP9Wh2AZMp6IQ5oqC2rOFE1hgvJXg3VuF/fg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.223.1.tgz", - "integrity": "sha512-TVbeqdBiYKS12OWZOk3y709l0yh66mCSAO8bSbIK8/Og3MQmy1vVBWEp+HbJvONmc02c226/8t2r7hmEgGcMvA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/otlp-transformer": "0.223.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.223.1.tgz", - "integrity": "sha512-22EJNk0qjI3klrON0aFR0BXye6FOM2vrkpjXjW8KDeSSiMez7+T4ARXfAh8+ucS3dEnhpSoGdfs4E/z4EIYjXQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-logs": "0.223.1", - "@opentelemetry/sdk-metrics": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.1.tgz", - "integrity": "sha512-IwrNcuLCvnJAnNLkXxx0Uw+rjBkVW6VGQrhd7PanNf0LavOzkTk9sDAjdqegM7p/VdxefNRrii6d+wBRJfz35g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-logs": { - "version": "0.223.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.223.1.tgz", - "integrity": "sha512-GpL453eVjCUn2fC3LYBuckCyPleN7FqpvmndT2kZ8gO8c6hHRL5X1BqLLDbVmdw1QDT4ZVyK9G4MNfz5xEkXew==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.223.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.12.1.tgz", - "integrity": "sha512-HeSblYhpds4QWliBE7ANCfoa2p3QH83uUxX4vO2qtk6BhIcqP7qDYYT36z3LOXftEBpdfbDStZxv+Cw5mqKX5g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.1.tgz", - "integrity": "sha512-vANdmCYnn+Ka/alBKxwICMl4Koa+aQBlRC9zAZy+Ig1Obbo3RZt2oiyv+8/gET5rrBBYrO3YniPcXfwZrN01rg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.12.1.tgz", - "integrity": "sha512-CMrcqFxTikYmYQCuHToBO4kQvWIseRk2ztzWidTd7VutIxj1rljvJZCZnFPJEGNOS7FvaSmRL21Jeuu3/wrCAQ==", + "version": "0.223.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.223.0.tgz", + "integrity": "sha512-PPfkgVBB9XVdANmSYeGfp1tG6RdsRFo9m9R6/ZnjvE9+TYTEXO2KYarsdtXQTdHhLSw7yzhSHqsNyUQ6c6W3eg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.12.1", - "@opentelemetry/resources": "2.12.1", - "@opentelemetry/sdk-trace": "2.12.1", + "@opentelemetry/api-logs": "0.223.0", + "@opentelemetry/configuration": "0.223.0", + "@opentelemetry/context-async-hooks": "2.12.0", + "@opentelemetry/core": "2.12.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.223.0", + "@opentelemetry/exporter-logs-otlp-http": "0.223.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.223.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.223.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.223.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.223.0", + "@opentelemetry/exporter-prometheus": "0.223.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.223.0", + "@opentelemetry/exporter-trace-otlp-http": "0.223.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.223.0", + "@opentelemetry/exporter-zipkin": "2.12.0", + "@opentelemetry/instrumentation": "0.223.0", + "@opentelemetry/otlp-exporter-base": "0.223.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.223.0", + "@opentelemetry/propagator-b3": "2.12.0", + "@opentelemetry/propagator-jaeger": "2.12.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/sdk-logs": "0.223.0", + "@opentelemetry/sdk-metrics": "2.12.0", + "@opentelemetry/sdk-trace": "2.12.0", + "@opentelemetry/sdk-trace-base": "2.12.0", + "@opentelemetry/sdk-trace-node": "2.12.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4496,23 +3494,6 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.12.1.tgz", - "integrity": "sha512-nRe0mimXZHknAEW2PKN9VbmML+93il5okotyDIUMhgYJ0QPM999fsh7djH6HDheyAbXO/C24t/B4fOQbNap8Vw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/context-async-hooks": "2.12.1", - "@opentelemetry/core": "2.12.1", - "@opentelemetry/sdk-trace-base": "2.12.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/sdk-trace": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.0.tgz", @@ -4567,18 +3548,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/context-async-hooks": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.12.0.tgz", - "integrity": "sha512-iK/1nD7mmZVkuYjVjkdFBiCvzntJdFny1S3CYbUYyOq7V5enL6XX+gJ7eh965L1LnS11VA8mQ7Ej/ck04SB0MQ==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/semantic-conventions": { "version": "1.38.0", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", @@ -8652,9 +7621,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.6.0.tgz", - "integrity": "sha512-XZ5apbzreCbXSKwPjJstu8R0zOi247w3XYwWxtwBXgpZrCyc1Ka7p9/xY0A9/diMewmHbEKz+FIraWaEWjlN5g==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", "license": "MIT", "engines": { "node": ">=12" @@ -10700,26 +9669,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-in-the-middle": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.6.0.tgz", - "integrity": "sha512-xRU7hI24S2FhucXtuZXn8EpcOjfVP6iiEQyR0USayPeaOroEmSsNlqnl95866iRdRtkHrd6Q/Z4D14R60au0+A==", - "license": "Apache-2.0", - "dependencies": { - "cjs-module-lexer": "^2.2.0", - "es-module-lexer": "^3.0.4", - "module-details-from-path": "^1.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/import-in-the-middle/node_modules/es-module-lexer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", - "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", - "license": "MIT" - }, "node_modules/import-without-cache": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.4.1.tgz", diff --git a/template/package.json b/template/package.json index 1c0e876ed..f7a027a69 100644 --- a/template/package.json +++ b/template/package.json @@ -79,6 +79,13 @@ "fast-uri": "3.1.0", "socks": "2.8.7", "bare-path": "3.1.1", - "bare-url": "2.5.2" + "bare-url": "2.5.2", + "import-in-the-middle": "3.5.3", + "@opentelemetry/instrumentation-bunyan": { + "@opentelemetry/semantic-conventions": "1.44.0" + }, + "@opentelemetry/instrumentation-pino": { + "@opentelemetry/semantic-conventions": "1.44.0" + } } } diff --git a/template/pnpm-lock.yaml b/template/pnpm-lock.yaml index 8b175bfbf..d99b50611 100644 --- a/template/pnpm-lock.yaml +++ b/template/pnpm-lock.yaml @@ -12,6 +12,8 @@ overrides: socks: 2.8.7 bare-path: 3.1.1 bare-url: 2.5.2 + '@opentelemetry/instrumentation-bunyan>@opentelemetry/semantic-conventions': 1.44.0 + '@opentelemetry/instrumentation-pino>@opentelemetry/semantic-conventions': 1.44.0 importers: @@ -1139,8 +1141,8 @@ packages: resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.45.0': - resolution: {integrity: sha512-aOP45mbcOKbQVvFWfQmgoOPT8R2iwbbbgl83k4CujGV9BrJhFr3qG55MFpwFs4YLabpuW3HhzUY9eQxPlDqaZw==} + '@opentelemetry/semantic-conventions@1.44.0': + resolution: {integrity: sha512-E3JkOlMBi3N8OGGkbl7tFyqk2MAS6/lySRZBK3Mu7AzjF1fT8oIChH9ChE48SUWzDn9Kdwta+G/p5LJ4xW4CLQ==} engines: {node: '>=14'} '@opentelemetry/sql-common@0.42.0': @@ -5438,7 +5440,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.223.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.45.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5684,7 +5686,7 @@ snapshots: '@opentelemetry/api-logs': 0.223.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.45.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5922,7 +5924,7 @@ snapshots: '@opentelemetry/semantic-conventions@1.38.0': {} - '@opentelemetry/semantic-conventions@1.45.0': {} + '@opentelemetry/semantic-conventions@1.44.0': {} '@opentelemetry/sql-common@0.42.0(@opentelemetry/api@1.9.0)': dependencies: diff --git a/template/pnpm-workspace.yaml b/template/pnpm-workspace.yaml index 96f3d8c12..3553d6952 100644 --- a/template/pnpm-workspace.yaml +++ b/template/pnpm-workspace.yaml @@ -9,6 +9,8 @@ overrides: socks: 2.8.7 bare-path: 3.1.1 bare-url: 2.5.2 + '@opentelemetry/instrumentation-bunyan>@opentelemetry/semantic-conventions': 1.44.0 + '@opentelemetry/instrumentation-pino>@opentelemetry/semantic-conventions': 1.44.0 allowBuilds: '@ast-grep/napi': true From 4480f2cb4b345905b0c4938466c473b29c81478c Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 10 Nov 2026 10:01:21 +0000 Subject: [PATCH 6/9] fix(deps): mirror the template pins in package.json and pnpm-workspace.yaml tools/check-template-deps.ts requires identical override keys in both files, and tools/check-template-artifact.ts expects string specifiers. Replace the scoped semantic-conventions pins with one flat 1.44.0 pin, and pin import-in-the-middle 3.5.3 for pnpm too. Both lockfiles stay clear of the versions the npm security policy blocks. Signed-off-by: Pedro Figueiredo --- template/package-lock.json | 24 +------- template/package.json | 7 +-- template/pnpm-lock.yaml | 108 +++++++++++++++++------------------ template/pnpm-workspace.yaml | 4 +- 4 files changed, 57 insertions(+), 86 deletions(-) diff --git a/template/package-lock.json b/template/package-lock.json index 5cb855409..bbff7facb 100644 --- a/template/package-lock.json +++ b/template/package-lock.json @@ -2567,15 +2567,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.44.0.tgz", - "integrity": "sha512-E3JkOlMBi3N8OGGkbl7tFyqk2MAS6/lySRZBK3Mu7AzjF1fT8oIChH9ChE48SUWzDn9Kdwta+G/p5LJ4xW4CLQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/instrumentation-cassandra-driver": { "version": "0.68.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.68.0.tgz", @@ -3054,15 +3045,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.44.0.tgz", - "integrity": "sha512-E3JkOlMBi3N8OGGkbl7tFyqk2MAS6/lySRZBK3Mu7AzjF1fT8oIChH9ChE48SUWzDn9Kdwta+G/p5LJ4xW4CLQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/instrumentation-redis": { "version": "0.71.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.71.0.tgz", @@ -3549,9 +3531,9 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", - "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.44.0.tgz", + "integrity": "sha512-E3JkOlMBi3N8OGGkbl7tFyqk2MAS6/lySRZBK3Mu7AzjF1fT8oIChH9ChE48SUWzDn9Kdwta+G/p5LJ4xW4CLQ==", "license": "Apache-2.0", "engines": { "node": ">=14" diff --git a/template/package.json b/template/package.json index f7a027a69..a5ca25916 100644 --- a/template/package.json +++ b/template/package.json @@ -81,11 +81,6 @@ "bare-path": "3.1.1", "bare-url": "2.5.2", "import-in-the-middle": "3.5.3", - "@opentelemetry/instrumentation-bunyan": { - "@opentelemetry/semantic-conventions": "1.44.0" - }, - "@opentelemetry/instrumentation-pino": { - "@opentelemetry/semantic-conventions": "1.44.0" - } + "@opentelemetry/semantic-conventions": "1.44.0" } } diff --git a/template/pnpm-lock.yaml b/template/pnpm-lock.yaml index d99b50611..c8cce617d 100644 --- a/template/pnpm-lock.yaml +++ b/template/pnpm-lock.yaml @@ -12,8 +12,8 @@ overrides: socks: 2.8.7 bare-path: 3.1.1 bare-url: 2.5.2 - '@opentelemetry/instrumentation-bunyan>@opentelemetry/semantic-conventions': 1.44.0 - '@opentelemetry/instrumentation-pino>@opentelemetry/semantic-conventions': 1.44.0 + import-in-the-middle: 3.5.3 + '@opentelemetry/semantic-conventions': 1.44.0 importers: @@ -1137,10 +1137,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/semantic-conventions@1.38.0': - resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} - engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.44.0': resolution: {integrity: sha512-E3JkOlMBi3N8OGGkbl7tFyqk2MAS6/lySRZBK3Mu7AzjF1fT8oIChH9ChE48SUWzDn9Kdwta+G/p5LJ4xW4CLQ==} engines: {node: '>=14'} @@ -3094,8 +3090,8 @@ packages: es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} - es-module-lexer@3.0.2: - resolution: {integrity: sha512-BuIB67FngDSyQ/dpQNOZybwdEBDUGJQvOqwWr4ha/ufYiqzuEwPkKO2zLhRAgay28tStRIHUeWmszZAJo3GCOg==} + es-module-lexer@3.0.4: + resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -3371,8 +3367,8 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-in-the-middle@3.5.1: - resolution: {integrity: sha512-mPKuL8bPQzecui2KK6Gb+M8JvJoHnhS1FeYGa22QopBmlevF5F0FE6ued/B5EgHDeIoMTONIpDWWFKUPOG0DBQ==} + import-in-the-middle@3.5.3: + resolution: {integrity: sha512-usm/IiY63VtOyx/dm7uEoZV+EsanCDmwRUroQZIkFLGu1NVXZ8mZqIOhaN4QIJ7EsbMTuQbJXwCySCx+ozMA8g==} engines: {node: '>=18'} import-without-cache@0.4.1: @@ -4809,7 +4805,7 @@ snapshots: '@opentelemetry/sdk-metrics': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': 2.12.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@standard-schema/spec': 1.1.0 '@types/semver': 7.7.1 apache-arrow: 21.1.0 @@ -5322,7 +5318,7 @@ snapshots: '@opentelemetry/core@2.12.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/exporter-logs-otlp-grpc@0.223.0(@opentelemetry/api@1.9.0)': dependencies: @@ -5375,7 +5371,7 @@ snapshots: '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.12.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/exporter-trace-otlp-grpc@0.223.0(@opentelemetry/api@1.9.0)': dependencies: @@ -5405,14 +5401,14 @@ snapshots: '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace': 2.12.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/instrumentation-amqplib@0.70.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5421,7 +5417,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-aws-xray': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@types/aws-lambda': 8.10.163 transitivePeerDependencies: - supports-color @@ -5431,7 +5427,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5448,7 +5444,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5457,7 +5453,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@types/connect': 3.4.38 transitivePeerDependencies: - supports-color @@ -5466,7 +5462,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5489,7 +5485,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5519,7 +5515,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5528,7 +5524,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5545,7 +5541,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color @@ -5555,7 +5551,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.3 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5563,7 +5559,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5571,7 +5567,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5580,7 +5576,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5595,7 +5591,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@types/memcached': 2.2.10 transitivePeerDependencies: - supports-color @@ -5604,7 +5600,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5613,7 +5609,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5621,7 +5617,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/sql-common': 0.42.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -5630,7 +5626,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5638,7 +5634,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5646,7 +5642,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5655,7 +5651,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.223.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5664,7 +5660,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5673,7 +5669,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/sql-common': 0.42.0(@opentelemetry/api@1.9.0) '@types/pg': 8.15.6 '@types/pg-pool': 2.0.7 @@ -5695,7 +5691,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.3 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5704,7 +5700,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5712,7 +5708,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5736,7 +5732,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5745,7 +5741,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.223.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5761,7 +5757,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.223.0 - import-in-the-middle: 3.5.1 + import-in-the-middle: 3.5.3 require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color @@ -5817,14 +5813,14 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/resource-detector-azure@0.31.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/resource-detector-container@0.8.14(@opentelemetry/api@1.9.0)': dependencies: @@ -5845,13 +5841,13 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/sdk-logs@0.223.0(@opentelemetry/api@1.9.0)': dependencies: @@ -5859,7 +5855,7 @@ snapshots: '@opentelemetry/api-logs': 0.223.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.12.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/sdk-metrics@2.12.0(@opentelemetry/api@1.9.0)': dependencies: @@ -5896,7 +5892,7 @@ snapshots: '@opentelemetry/sdk-trace': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': 2.12.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 transitivePeerDependencies: - supports-color @@ -5906,7 +5902,7 @@ snapshots: '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace': 2.12.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/sdk-trace-node@2.12.0(@opentelemetry/api@1.9.0)': dependencies: @@ -5920,9 +5916,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.12.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 - - '@opentelemetry/semantic-conventions@1.38.0': {} + '@opentelemetry/semantic-conventions': 1.44.0 '@opentelemetry/semantic-conventions@1.44.0': {} @@ -7515,7 +7509,7 @@ snapshots: es-module-lexer@2.3.2: {} - es-module-lexer@3.0.2: {} + es-module-lexer@3.0.4: {} es-object-atoms@1.1.2: dependencies: @@ -7864,10 +7858,10 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-in-the-middle@3.5.1: + import-in-the-middle@3.5.3: dependencies: cjs-module-lexer: 2.2.1 - es-module-lexer: 3.0.2 + es-module-lexer: 3.0.4 module-details-from-path: 1.0.4 import-without-cache@0.4.1: {} diff --git a/template/pnpm-workspace.yaml b/template/pnpm-workspace.yaml index 3553d6952..0617ce9b3 100644 --- a/template/pnpm-workspace.yaml +++ b/template/pnpm-workspace.yaml @@ -9,8 +9,8 @@ overrides: socks: 2.8.7 bare-path: 3.1.1 bare-url: 2.5.2 - '@opentelemetry/instrumentation-bunyan>@opentelemetry/semantic-conventions': 1.44.0 - '@opentelemetry/instrumentation-pino>@opentelemetry/semantic-conventions': 1.44.0 + import-in-the-middle: 3.5.3 + '@opentelemetry/semantic-conventions': 1.44.0 allowBuilds: '@ast-grep/napi': true From a0699fe4a5a5cdd84f8dddb950f993382a1e6abb Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 10 Nov 2026 10:23:27 +0000 Subject: [PATCH 7/9] refactor(appkit): trim the Cloud Fetch fallback docs and drop a redundant Null branch Shorten docblocks to their contracts, drop a JSDoc that repeated the TTL note, and fix the TTL comment that still said only refusals are remembered. vectorFromArray takes the rebuilt type without a cast, and an all-null VOID column needs no special case. Signed-off-by: Pedro Figueiredo --- .../connectors/sql-warehouse/arrow-schema.ts | 21 +++--------- .../sql-warehouse/json-array-cells.ts | 8 ++--- .../appkit/src/plugins/analytics/analytics.ts | 6 +--- .../src/plugins/analytics/result-delivery.ts | 34 +++++++------------ .../src/stream/arrow-stream-processor.ts | 12 +++---- 5 files changed, 25 insertions(+), 56 deletions(-) diff --git a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts index 806e8f238..aaf0d7ade 100644 --- a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts +++ b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts @@ -78,15 +78,10 @@ export function buildEmptyArrowIPCBase64(columns: ColumnInfo[]): string { } /** - * Rebuild the Arrow IPC stream a warehouse serves for `ARROW_STREAM` from the - * positional string rows of an `INLINE` + `JSON_ARRAY` result. Fields carry the - * manifest column names, duplicates included, like the native Cloud Fetch - * stream, and each type is what {@link parseDatabricksType} gives that column, - * with DECIMAL precision and scale taken from `typePrecision` and `typeScale`. - * - * Nested, interval, and geospatial columns arrive as strings on the JSON wire - * and stay strings, and so does any type without a {@link parseJsonArrayCell} - * codec. A cell that does not parse as its column type throws. + * Rebuild the `ARROW_STREAM` IPC bytes from the positional rows of an `INLINE` + * + `JSON_ARRAY` result, keeping the manifest column names (duplicates + * included). Columns without a {@link parseJsonArrayCell} codec stay Utf8, and + * a cell that does not parse as its column type throws. */ export function buildArrowIPCFromRows( columns: ColumnInfo[], @@ -116,19 +111,13 @@ export function buildArrowIPCFromRows( function vectorFromCells(col: ColumnInfo, cells: Array) { const type = rebuildType(col); - if (type.typeId === Type.Null) { - return vectorFromArray( - cells.map(() => null), - type as never, - ); - } const values = cells.map((cell) => cell === null ? null : parseJsonArrayCell(cell, type), ); if (type.typeId === Type.Decimal) { return decimalVector(values as Array, type as Decimal); } - return vectorFromArray(values, type as never); + return vectorFromArray(values, type); } const STRING_ONLY_TYPES = new Set([ diff --git a/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts b/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts index e08d91a9d..1a2584cd3 100644 --- a/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts +++ b/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts @@ -1,11 +1,9 @@ import { type DataType, Type } from "apache-arrow"; /** - * How one scalar type is written in an `INLINE` + `JSON_ARRAY` result. - * `format` turns an Arrow value into the warehouse's text, and `parse` turns - * that text back into the value `vectorFromArray` accepts (an unscaled `bigint` - * for DECIMAL, epoch milliseconds for DATE and TIMESTAMP). The text forms are - * pinned to a captured warehouse response in `tests/fixtures/typed-values.json`. + * How one scalar type is written in an `INLINE` + `JSON_ARRAY` cell. `parse` + * returns what `vectorFromArray` accepts: an unscaled `bigint` for DECIMAL, + * epoch milliseconds for DATE and TIMESTAMP. */ interface CellCodec { format(value: unknown, type: DataType): string; diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 7834114f2..55b9bea28 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -976,10 +976,6 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { } } - /** - * The remembered arrow capability for a warehouse. A lapsed `"links-denied"` - * reverts to `"external"`, so the next query tries Cloud Fetch again. - */ private _arrowCapabilityHint( warehouseId: string, ): ArrowCapability | undefined { @@ -1246,7 +1242,7 @@ export function writeChunk( */ const DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000; -/** How long a definite Cloud Fetch refusal is remembered before storage is tried again. */ +/** How long `"links-denied"` is remembered before Cloud Fetch is tried again. */ const LINKS_DENIED_TTL_MS = 10 * 60_000; /** diff --git a/packages/appkit/src/plugins/analytics/result-delivery.ts b/packages/appkit/src/plugins/analytics/result-delivery.ts index b73bc7c3d..ffb2da354 100644 --- a/packages/appkit/src/plugins/analytics/result-delivery.ts +++ b/packages/appkit/src/plugins/analytics/result-delivery.ts @@ -76,9 +76,8 @@ export interface ArrowChunkStreamer { } /** - * The arrow delivery mode a warehouse actually supports. `"links-denied"` means - * storage refused or never answered the Cloud Fetch download, so results are served - * from an inline `JSON_ARRAY` run until the caller lets the hint lapse. + * The arrow delivery mode a warehouse actually supports. `"links-denied"`: + * storage refused or never answered Cloud Fetch, so inline `JSON_ARRAY` goes first. */ export type ArrowCapability = "inline" | "external" | "links-denied"; @@ -315,11 +314,7 @@ class InlineDoesNotFit extends Error {} type InlineAttempt = { bytes: Uint8Array } | { error: unknown }; -/** - * Run the query as `INLINE` + `JSON_ARRAY` and rebuild the Arrow IPC stream a - * Cloud Fetch download would have carried. The route sees the same bytes and - * header names either way. - */ +/** Run the query as `INLINE` + `JSON_ARRAY` and rebuild the Arrow bytes Cloud Fetch would have served. */ async function inlineJsonAsArrow( executor: QueryExecutor, query: string, @@ -387,21 +382,16 @@ function linksDenied(cause: ExecutionError): ExecutionError { } /** - * {@link deliverArrowBytes} with a way out when the Cloud Fetch download fails - * before its first byte (storage refuses, resets, or never answers). - * - * The first chunk is pulled here, before the route sends headers, so a failed - * download can still be replaced by `inlineJsonAsArrow`. A 400 or 403 that - * survived the link re-mint reports `"links-denied"` at once. No response at all - * reports it once the inline result has been served, or once the patient pass - * also fails, because blocked hosts hang instead of refusing. A network error - * or body failure falls back once and is forgotten. A result too large for - * inline is never stranded: after an inconclusive failure Cloud Fetch gets one - * more, patient pass, and after a definite refusal the error says why. Any - * other inline failure is rethrown unchanged. + * {@link deliverArrowBytes}, falling back to an inline `JSON_ARRAY` run when the + * Cloud Fetch download fails before its first byte (pulled here, before the + * route sends headers). * - * With the `"links-denied"` hint the inline run goes first, and Cloud Fetch is - * tried only if that cannot deliver. + * A 400/403 that survives the re-mint reports `"links-denied"` at once; a + * download that never answers reports it after the fallback, since blocked + * hosts hang instead of refusing. Other download errors fall back without being + * remembered. A result too large for inline gets one patient Cloud Fetch retry, + * or `EXTERNAL_LINKS_DENIED` after a refusal. With the `"links-denied"` hint, + * inline runs first. */ export async function* deliverArrowBytesWithFallback( executor: QueryExecutor, diff --git a/packages/appkit/src/stream/arrow-stream-processor.ts b/packages/appkit/src/stream/arrow-stream-processor.ts index 2021eddf9..4c8cfd252 100644 --- a/packages/appkit/src/stream/arrow-stream-processor.ts +++ b/packages/appkit/src/stream/arrow-stream-processor.ts @@ -112,10 +112,9 @@ export class ArrowStreamProcessor { * so a mid-body failure propagates and the caller aborts the response. An * idle timeout, reset before every read, aborts a stalled download. * - * A 403 re-mints the link once and retries at once, since a pre-signed URL - * may have expired. Any other 4xx except 408 and 429 stops, and so does a - * wait for headers that outlasts the limit: a blocked host can hang for - * minutes instead of refusing, and a retry would only repeat the wait. + * A 403 re-mints the link once, since a pre-signed URL may have expired. Other + * 4xx (except 408/429) and header timeouts stop: a blocked host hangs instead + * of refusing, so a retry only repeats the wait. */ private async *streamChunkBody( chunk: ExternalLink, @@ -247,10 +246,7 @@ export class ArrowStreamProcessor { } } - /** - * One GET. Only the wait for response headers is timed here; the timer is - * cleared the moment they arrive, so a healthy chunk may stream for longer. - */ + /** One GET; only the wait for response headers is timed. */ private async connect( link: ExternalLink, headerWaitMs: number, From c30d3f035c7ad08140c13e6033a4bf5378d862f0 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 10 Nov 2026 11:41:06 +0000 Subject: [PATCH 8/9] fix(appkit): tighten the Cloud Fetch fallback's error rules and cell parsing Review follow-ups for LA-2169: - With the links-denied hint, an inline failure that isn't about size is rethrown at once instead of running the query again over Cloud Fetch. - EXTERNAL_LINKS_DENIED only when storage refused or never answered; any other failure of the full-header-wait retry keeps its own error. A download that never answered is remembered as links-denied even when the retry then fails another way. - The client message is cloud-neutral, and each switch to inline is logged. - Cells parse strictly: decimal or scientific floats, base64 binary, real calendar dates, and decimals within their precision. - Unnamed rebuilt columns are column_N, matching the X-Appkit-Arrow-Columns header. - downloadFailureKind() sits next to the error it reads, one first-chunk helper replaces two copies, the capability memo keeps its expiry in the same map, and `patient` is now `fullHeaderWait`. - The typed-values fixture is re-captured with nulls in every column. Signed-off-by: Pedro Figueiredo --- .../connectors/sql-warehouse/arrow-schema.ts | 54 +++-- .../src/connectors/sql-warehouse/client.ts | 20 +- .../sql-warehouse/json-array-cells.ts | 56 +++-- .../sql-warehouse/tests/arrow-schema.test.ts | 9 +- .../tests/fixtures/typed-values.json | 186 +++++++-------- .../tests/json-array-cells.test.ts | 14 ++ .../appkit/src/plugins/analytics/analytics.ts | 38 ++- .../src/plugins/analytics/result-delivery.ts | 221 +++++++++--------- .../analytics/tests/result-delivery.test.ts | 34 ++- .../src/stream/arrow-stream-processor.ts | 70 ++++-- .../tests/arrow-stream-processor.test.ts | 2 +- 11 files changed, 392 insertions(+), 312 deletions(-) diff --git a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts index aaf0d7ade..bf4bc480b 100644 --- a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts +++ b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts @@ -60,28 +60,43 @@ export function parseDatabricksType(typeText: string): DataType { * contract. */ export function buildEmptyArrowIPCBase64(columns: ColumnInfo[]): string { - const fields = columns.map((col, index) => { - const typeText = col.typeText ?? col.typeName ?? "STRING"; - let dataType: DataType; - try { - dataType = parseDatabricksType(typeText); - } catch { - dataType = new Utf8(); - } - const name = col.name && col.name.length > 0 ? col.name : `column_${index}`; - return new Field(name, dataType, true); - }); + const fields = columns.map( + (col, index) => + new Field(manifestColumnName(col, index), columnArrowType(col), true), + ); const schema = new Schema(fields); const table = new Table(schema); const ipc = tableToIPC(table, "stream"); return Buffer.from(ipc).toString("base64"); } +/** A manifest column's name, or `column_N` when it has none. */ +export function manifestColumnName( + col: ColumnInfo | undefined, + index: number, +): string { + return col?.name || `column_${index}`; +} + +function columnTypeText(col: ColumnInfo): string { + return col.typeText ?? col.typeName ?? "STRING"; +} + +/** The Arrow type for a manifest column; unparseable type text becomes Utf8. */ +function columnArrowType(col: ColumnInfo): DataType { + try { + return parseDatabricksType(columnTypeText(col)); + } catch { + return new Utf8(); + } +} + /** * Rebuild the `ARROW_STREAM` IPC bytes from the positional rows of an `INLINE` * + `JSON_ARRAY` result, keeping the manifest column names (duplicates * included). Columns without a {@link parseJsonArrayCell} codec stay Utf8, and - * a cell that does not parse as its column type throws. + * a cell that does not parse as its column type throws. Zoned TIMESTAMP columns + * are labelled UTC, since cells carry an offset rather than the session zone. */ export function buildArrowIPCFromRows( columns: ColumnInfo[], @@ -95,7 +110,7 @@ export function buildArrowIPCFromRows( ); const fields = vectors.map( (vector, index) => - new Field(columns[index].name || `col_${index}`, vector.type, true), + new Field(manifestColumnName(columns[index], index), vector.type, true), ); const batch = new RecordBatch( new Schema(fields), @@ -129,16 +144,13 @@ const STRING_ONLY_TYPES = new Set([ ]); function rebuildType(col: ColumnInfo): DataType { - const typeText = col.typeText ?? col.typeName ?? "STRING"; - let type: DataType; - try { - type = parseDatabricksType(typeText); - } catch { - return new Utf8(); - } + const type = columnArrowType(col); if (STRING_ONLY_TYPES.has(type.typeId)) return new Utf8(); // GEOGRAPHY and GEOMETRY map to Binary but arrive as text on the JSON wire. - if (type.typeId === Type.Binary && typeText.toUpperCase() !== "BINARY") { + if ( + type.typeId === Type.Binary && + columnTypeText(col).toUpperCase() !== "BINARY" + ) { return new Utf8(); } if (type.typeId === Type.Decimal && col.typePrecision != null) { diff --git a/packages/appkit/src/connectors/sql-warehouse/client.ts b/packages/appkit/src/connectors/sql-warehouse/client.ts index 73d6128a9..306224ff9 100644 --- a/packages/appkit/src/connectors/sql-warehouse/client.ts +++ b/packages/appkit/src/connectors/sql-warehouse/client.ts @@ -31,7 +31,7 @@ import type { StatementStatus, WorkspaceClient, } from "../../workspace-client"; -import { buildEmptyArrowIPCBase64 } from "./arrow-schema"; +import { buildEmptyArrowIPCBase64, manifestColumnName } from "./arrow-schema"; import { executeStatementDefaults } from "./defaults"; import { WarehousePollBackoff } from "./warehouse-poll-backoff"; import { WarehouseStatusEmitter } from "./warehouse-status-emitter"; @@ -48,15 +48,12 @@ const logger = createLogger("connectors:sql-warehouse"); function arrowColumnNames(response: StatementResponse): string[] | undefined { const cols = response.manifest?.schema?.columns; if (!cols || cols.length === 0) return undefined; - return cols.map((c, i) => - c.name && c.name.length > 0 ? c.name : `column_${i}`, - ); + return cols.map(manifestColumnName); } /** * Coerce the modular SDK's `bigint` row/byte counts back to `number` (the type - * the legacy SDK used). AppKit never does arithmetic on these — they are purely - * informational — but a stray `bigint` makes `JSON.stringify` throw ("Do not + * the legacy SDK used). A stray `bigint` makes `JSON.stringify` throw ("Do not * know how to serialize a BigInt") the instant the result is cached or written * to an SSE frame. Reyden's INLINE + ARROW_STREAM result — which the analytics * arrow path caches — carries them on `result`/`manifest`, so normalize every @@ -1030,7 +1027,7 @@ export class SQLWarehouseConnector { const obj: Record = {}; row.forEach((value, index) => { const column = columns[index]; - const columnName = column?.name || `column_${index}`; + const columnName = manifestColumnName(column, index); // attempt to parse JSON strings for string columns if ( @@ -1251,9 +1248,14 @@ export class SQLWarehouseConnector { chunks: ExternalLink[], signal?: AbortSignal, refresh?: RefreshChunkLink, - patient?: boolean, + fullHeaderWait?: boolean, ): AsyncGenerator { - return this.arrowProcessor.streamChunks(chunks, signal, refresh, patient); + return this.arrowProcessor.streamChunks( + chunks, + signal, + refresh, + fullHeaderWait, + ); } /** diff --git a/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts b/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts index 1a2584cd3..7c61f0517 100644 --- a/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts +++ b/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts @@ -6,11 +6,15 @@ import { type DataType, Type } from "apache-arrow"; * epoch milliseconds for DATE and TIMESTAMP. */ interface CellCodec { - format(value: unknown, type: DataType): string; + /** Defaults to `String(value)`, or base64 for bytes. */ + format?(value: unknown, type: DataType): string; parse(cell: string, type: DataType): unknown; } const DECIMAL_TEXT = /^(-?)(\d+)(?:\.(\d+))?$/; +const FLOAT_TEXT = + /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$|^(?:NaN|-?Infinity)$/; +const BASE64_TEXT = /^[A-Za-z0-9+/]*={0,2}$/; const DATE_TEXT = /^\d{4}-\d{2}-\d{2}$/; /** `TIMESTAMP` cells carry the session offset (or `Z`); `TIMESTAMP_NTZ` cells carry none. */ const ZONED_TIMESTAMP_TEXT = @@ -21,9 +25,18 @@ function invalid(kind: string, cell: string): Error { return new Error(`Invalid ${kind} "${cell}"`); } +/** `Date.parse` rolls 2026-02-31 over to March, so the day must survive a round trip. */ function epochMillis(text: string, kind: string, cell: string): number { + const day = cell.slice(0, 10); const ms = Date.parse(text); - if (Number.isNaN(ms)) throw invalid(kind, cell); + const dayMs = Date.parse(`${day}T00:00:00Z`); + if ( + Number.isNaN(ms) || + Number.isNaN(dayMs) || + !new Date(dayMs).toISOString().startsWith(day) + ) { + throw invalid(kind, cell); + } return ms; } @@ -32,7 +45,6 @@ const hasTimezone = (type: DataType) => const CODECS: Partial> = { [Type.Int]: { - format: (value) => String(value), parse(cell, type) { const { bitWidth } = type as DataType & { bitWidth: number }; if (bitWidth === 64) { @@ -59,13 +71,9 @@ const CODECS: Partial> = { }, }, [Type.Float]: { - format: (value) => String(value), parse(cell) { - const n = Number(cell); - if (cell.trim() === "" || (Number.isNaN(n) && cell !== "NaN")) { - throw invalid("number", cell); - } - return n; + if (!FLOAT_TEXT.test(cell)) throw invalid("number", cell); + return Number(cell); }, }, [Type.Decimal]: { @@ -81,11 +89,21 @@ const CODECS: Partial> = { return negative ? `-${out}` : out; }, parse(cell, type) { - const { scale } = type as DataType & { scale: number }; + const { scale, precision } = type as DataType & { + scale: number; + precision: number; + }; const match = DECIMAL_TEXT.exec(cell); - const [, sign, whole, fraction = ""] = match ?? []; - if (!match || fraction.length > scale) throw invalid("decimal", cell); - const unscaled = BigInt(`${whole}${fraction.padEnd(scale, "0")}`); + if (!match) throw invalid("decimal", cell); + const [, sign, whole, fraction = ""] = match; + const digits = `${whole}${fraction.padEnd(scale, "0")}`; + if ( + fraction.length > scale || + digits.replace(/^0+/, "").length > precision + ) { + throw invalid("decimal", cell); + } + const unscaled = BigInt(digits); return sign === "-" ? -unscaled : unscaled; }, }, @@ -111,18 +129,18 @@ const CODECS: Partial> = { }, }, [Type.Bool]: { - format: (value) => String(value), parse(cell) { if (cell !== "true" && cell !== "false") throw invalid("boolean", cell); return cell === "true"; }, }, [Type.Binary]: { - format: (value) => Buffer.from(value as Uint8Array).toString("base64"), - parse: (cell) => new Uint8Array(Buffer.from(cell, "base64")), + parse(cell) { + if (!BASE64_TEXT.test(cell)) throw invalid("binary", cell); + return new Uint8Array(Buffer.from(cell, "base64")); + }, }, [Type.Utf8]: { - format: (value) => String(value), parse: (cell) => cell, }, }; @@ -134,8 +152,8 @@ export function hasJsonArrayCodec(type: DataType): boolean { /** Render a scalar Arrow value the way the warehouse writes it under `JSON_ARRAY`. */ export function formatJsonArrayCell(value: unknown, type: DataType): string { - const codec = CODECS[type.typeId]; - if (codec) return codec.format(value, type); + const format = CODECS[type.typeId]?.format; + if (format) return format(value, type); if (value instanceof Uint8Array) return Buffer.from(value).toString("base64"); if (value instanceof Date) return value.toISOString(); return String(value); diff --git a/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts b/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts index 6effa1078..244138d7a 100644 --- a/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts +++ b/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts @@ -679,6 +679,11 @@ describe("buildArrowIPCFromRows", () => { expect(table.getChildAt(2)?.get(0)).toBe(payload); }); + test("an unnamed column gets the column_N name the header carries", () => { + const ipc = buildArrowIPCFromRows([{ name: "", typeText: "INT" }], [["1"]]); + expect(tableFromIPC(ipc).schema.fields[0].name).toBe("column_0"); + }); + test("nested, interval and geospatial columns stay strings", () => { const columns = [ { name: "a", typeText: "ARRAY" }, @@ -796,7 +801,9 @@ describe("buildArrowIPCFromRows against a captured warehouse response", () => { const index = fixture.columns.findIndex((c) => c.name === "ts_micro"); expect(fixture.dataArray[0][index]).toMatch(/\.\d{3}-\d{2}:\d{2}$/); expect(values(rebuilt, index)).toEqual( - values(native, index).map((ms) => Math.trunc(ms as number)), + values(native, index).map((ms) => + ms === null ? null : Math.trunc(ms as number), + ), ); }); diff --git a/packages/appkit/src/connectors/sql-warehouse/tests/fixtures/typed-values.json b/packages/appkit/src/connectors/sql-warehouse/tests/fixtures/typed-values.json index 57fb0d722..e3872788a 100644 --- a/packages/appkit/src/connectors/sql-warehouse/tests/fixtures/typed-values.json +++ b/packages/appkit/src/connectors/sql-warehouse/tests/fixtures/typed-values.json @@ -1,6 +1,6 @@ { - "source": "Statement Execution API on a serverless PRO SQL warehouse (session time zone America/Los_Angeles), captured 2026-11-09", - "statement": "SELECT\n CAST(id AS INT) AS id,\n CAST(id AS BIGINT) * 1000000000007 AS big,\n CAST(id % 100 AS TINYINT) AS tiny,\n CAST(id AS SMALLINT) AS small,\n CAST(id * 1.5 AS DOUBLE) AS amount,\n CAST(id * 0.25 AS FLOAT) AS ratio,\n CAST((id * 101 - 50) / 100.0 AS DECIMAL(10, 2)) AS price,\n DATE_ADD(DATE'2026-01-01', CAST(id AS INT)) AS day,\n TIMESTAMPADD(HOUR, id, TIMESTAMP'2026-01-01 10:00:00') AS ts,\n TIMESTAMPADD(HOUR, id, TIMESTAMP_NTZ'2026-01-01 10:00:00') AS ts_ntz,\n TIMESTAMPADD(MICROSECOND, id, TIMESTAMP'2026-01-01 10:00:00.123456') AS ts_micro,\n CONCAT('{\"order_id\": 1234567890123456789', CAST(id % 10 AS STRING), ', \"tags\": [1, 2]}') AS payload,\n id % 2 = 0 AS flag,\n CASE WHEN id % 7 = 3 THEN NULL ELSE CONCAT('row-', CAST(id AS STRING)) END AS label,\n ARRAY(id, id + 1) AS arr,\n id AS dup,\n id + 1 AS dup\nFROM range(0, 30) AS t(id)\nORDER BY id", + "source": "Statement Execution API on a serverless PRO SQL warehouse (session time zone America/Los_Angeles), captured 2026-11-10", + "statement": "SELECT\n CAST(id AS INT) AS id,\n IF(id % 5 = 4, NULL, CAST(id AS BIGINT) * 1000000000007) AS big,\n IF(id % 5 = 4, NULL, CAST(id % 100 AS TINYINT)) AS tiny,\n IF(id % 5 = 4, NULL, CAST(id AS SMALLINT)) AS small,\n IF(id % 5 = 4, NULL, CAST(id * 1.5 AS DOUBLE)) AS amount,\n IF(id % 5 = 4, NULL, CAST(id * 0.25 AS FLOAT)) AS ratio,\n IF(id % 5 = 4, NULL, CAST((id * 101 - 50) / 100.0 AS DECIMAL(10, 2))) AS price,\n IF(id % 5 = 4, NULL, DATE_ADD(DATE'2026-01-01', CAST(id AS INT))) AS day,\n IF(id % 5 = 4, NULL, TIMESTAMPADD(HOUR, id, TIMESTAMP'2026-01-01 10:00:00')) AS ts,\n IF(id % 5 = 4, NULL, TIMESTAMPADD(HOUR, id, TIMESTAMP_NTZ'2026-01-01 10:00:00')) AS ts_ntz,\n IF(id % 5 = 4, NULL, TIMESTAMPADD(MICROSECOND, id, TIMESTAMP'2026-01-01 10:00:00.123456')) AS ts_micro,\n IF(id % 5 = 4, NULL, CONCAT('{\"order_id\": 1234567890123456789', CAST(id % 10 AS STRING), ', \"tags\": [1, 2]}')) AS payload,\n IF(id % 5 = 4, NULL, id % 2 = 0) AS flag,\n CASE WHEN id % 7 = 3 THEN NULL ELSE CONCAT('row-', CAST(id AS STRING)) END AS label,\n IF(id % 5 = 4, NULL, ARRAY(id, id + 1)) AS arr,\n IF(id % 5 = 4, NULL, id) AS dup,\n IF(id % 5 = 4, NULL, id + 1) AS dup\nFROM range(0, 30) AS t(id)\nORDER BY id", "columns": [ { "name": "id", @@ -186,22 +186,22 @@ ], [ "4", - "4000000000028", - "4", - "4", - "6.0", - "1.0", - "3.54", - "2026-01-05", - "2026-01-01T14:00:00.000-08:00", - "2026-01-01T14:00:00.000", - "2026-01-01T10:00:00.123-08:00", - "{\"order_id\": 12345678901234567894, \"tags\": [1, 2]}", - "true", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, "row-4", - "[\"4\",\"5\"]", - "4", - "5" + null, + null, + null ], [ "5", @@ -281,22 +281,22 @@ ], [ "9", - "9000000000063", - "9", - "9", - "13.5", - "2.25", - "8.59", - "2026-01-10", - "2026-01-01T19:00:00.000-08:00", - "2026-01-01T19:00:00.000", - "2026-01-01T10:00:00.123-08:00", - "{\"order_id\": 12345678901234567899, \"tags\": [1, 2]}", - "false", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, "row-9", - "[\"9\",\"10\"]", - "9", - "10" + null, + null, + null ], [ "10", @@ -376,22 +376,22 @@ ], [ "14", - "14000000000098", - "14", - "14", - "21.0", - "3.5", - "13.64", - "2026-01-15", - "2026-01-02T00:00:00.000-08:00", - "2026-01-02T00:00:00.000", - "2026-01-01T10:00:00.123-08:00", - "{\"order_id\": 12345678901234567894, \"tags\": [1, 2]}", - "true", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, "row-14", - "[\"14\",\"15\"]", - "14", - "15" + null, + null, + null ], [ "15", @@ -471,22 +471,22 @@ ], [ "19", - "19000000000133", - "19", - "19", - "28.5", - "4.75", - "18.69", - "2026-01-20", - "2026-01-02T05:00:00.000-08:00", - "2026-01-02T05:00:00.000", - "2026-01-01T10:00:00.123-08:00", - "{\"order_id\": 12345678901234567899, \"tags\": [1, 2]}", - "false", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, "row-19", - "[\"19\",\"20\"]", - "19", - "20" + null, + null, + null ], [ "20", @@ -566,22 +566,22 @@ ], [ "24", - "24000000000168", - "24", - "24", - "36.0", - "6.0", - "23.74", - "2026-01-25", - "2026-01-02T10:00:00.000-08:00", - "2026-01-02T10:00:00.000", - "2026-01-01T10:00:00.123-08:00", - "{\"order_id\": 12345678901234567894, \"tags\": [1, 2]}", - "true", null, - "[\"24\",\"25\"]", - "24", - "25" + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null ], [ "25", @@ -661,23 +661,23 @@ ], [ "29", - "29000000000203", - "29", - "29", - "43.5", - "7.25", - "28.79", - "2026-01-30", - "2026-01-02T15:00:00.000-08:00", - "2026-01-02T15:00:00.000", - "2026-01-01T10:00:00.123-08:00", - "{\"order_id\": 12345678901234567899, \"tags\": [1, 2]}", - "false", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, "row-29", - "[\"29\",\"30\"]", - "29", - "30" + null, + null, + null ] ], - "arrowIpcBase64": "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" + "arrowIpcBase64": "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" } diff --git a/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts b/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts index 19ec1fe11..c05a84d7f 100644 --- a/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts +++ b/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts @@ -181,6 +181,8 @@ describe("JSON_ARRAY cell codecs", () => { ["BIGINT with a leading zero", new Int64(), "01"], ["empty DOUBLE", new Float64(), ""], ["DOUBLE that is not a number", new Float64(), "abc"], + ["DOUBLE in hex", new Float64(), "0x1A"], + ["DOUBLE with padding", new Float64(), " 1 "], [ "DECIMAL with more digits than its scale", new Decimal(2, 10, 128), @@ -188,6 +190,13 @@ describe("JSON_ARRAY cell codecs", () => { ], ["DECIMAL with two points", new Decimal(2, 10, 128), "1.2.3"], ["DECIMAL with a plus sign", new Decimal(2, 10, 128), "+1.00"], + [ + "DECIMAL with more digits than its precision", + new Decimal(2, 5, 128), + "123456789.00", + ], + ["BINARY that is not base64", new Binary(), "not base64!!"], + ["DATE that is not on the calendar", new DateDay(), "2026-02-31"], ["DATE without zero padding", new DateDay(), "2026-1-1"], ["DATE with a time", new DateDay(), "2026-01-01T00:00:00Z"], [ @@ -200,6 +209,11 @@ describe("JSON_ARRAY cell codecs", () => { new TimestampMicrosecond("UTC"), "2026-13-01T10:00:00Z", ], + [ + "TIMESTAMP_NTZ that is not on the calendar", + new TimestampMicrosecond(), + "2026-02-30T10:00:00.000", + ], [ "TIMESTAMP_NTZ with Z", new TimestampMicrosecond(), diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 55b9bea28..c6b1bca07 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -130,12 +130,13 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { * on every query, so once learned we skip that doomed probe; Reyden stays * `"inline"`. Capability is a property of the warehouse, not the user, so it * is not user-scoped. Bounded by the number of distinct warehouses a process - * talks to (effectively one). + * talks to (effectively one). `"links-denied"` reads as `"external"` once + * `until` passes. */ - private _arrowCapability = new Map(); - - /** When each warehouse's `"links-denied"` entry lapses back to `"external"`. */ - private _linksDeniedUntil = new Map(); + private _arrowCapability = new Map< + string, + { capability: ArrowCapability; until?: number } + >(); constructor(config: IAnalyticsConfig) { super(config); @@ -890,15 +891,14 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { // Skip the doomed INLINE probe on a warehouse already known to need // EXTERNAL_LINKS; remember the resolved mode for next time. capabilityHint: this._arrowCapabilityHint(warehouseId), - onCapabilityResolved: (capability) => { - this._arrowCapability.set(warehouseId, capability); - if (capability === "links-denied") { - this._linksDeniedUntil.set( - warehouseId, - Date.now() + LINKS_DENIED_TTL_MS, - ); - } - }, + onCapabilityResolved: (capability) => + this._arrowCapability.set(warehouseId, { + capability, + until: + capability === "links-denied" + ? Date.now() + LINKS_DENIED_TTL_MS + : undefined, + }), }, ); const first = await bytes.next(); @@ -979,15 +979,11 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider { private _arrowCapabilityHint( warehouseId: string, ): ArrowCapability | undefined { - const capability = this._arrowCapability.get(warehouseId); - if ( - capability === "links-denied" && - Date.now() >= (this._linksDeniedUntil.get(warehouseId) ?? 0) - ) { - this._arrowCapability.set(warehouseId, "external"); + const entry = this._arrowCapability.get(warehouseId); + if (entry?.until !== undefined && Date.now() >= entry.until) { return "external"; } - return capability; + return entry?.capability; } /** diff --git a/packages/appkit/src/plugins/analytics/result-delivery.ts b/packages/appkit/src/plugins/analytics/result-delivery.ts index ffb2da354..c08bf492a 100644 --- a/packages/appkit/src/plugins/analytics/result-delivery.ts +++ b/packages/appkit/src/plugins/analytics/result-delivery.ts @@ -7,7 +7,8 @@ import { formatJsonArrayCell } from "../../connectors/sql-warehouse/json-array-c import { ExecutionError } from "../../errors"; import { createLogger } from "../../logging/logger"; import { - EXTERNAL_LINK_DOWNLOAD_FAILED, + type DownloadFailureKind, + downloadFailureKind, type RefreshChunkLink, } from "../../stream/arrow-stream-processor"; import type { ColumnInfo, ExternalLink } from "../../workspace-client"; @@ -71,7 +72,7 @@ export interface ArrowChunkStreamer { chunks: ExternalLink[], signal?: AbortSignal, refresh?: RefreshChunkLink, - patient?: boolean, + fullHeaderWait?: boolean, ): AsyncGenerator; } @@ -88,7 +89,7 @@ export interface ArrowDeliveryOut { /** Set once EXTERNAL_LINKS resolves. */ cloudFetch?: { totalByteCount?: number; - stream(patient: boolean): AsyncGenerator; + stream(fullHeaderWait: boolean): AsyncGenerator; }; } @@ -303,8 +304,13 @@ export async function* deliverArrowBytes( // mismatched identity. `refreshChunkLink` (also bound to this context) lets // the streamer re-mint a link that expires mid-download. const links = ext.external_links; - const stream = (patient: boolean) => - streamer.streamExternalLinks(links, signal, ext.refreshChunkLink, patient); + const stream = (fullHeaderWait: boolean) => + streamer.streamExternalLinks( + links, + signal, + ext.refreshChunkLink, + fullHeaderWait, + ); out.cloudFetch = { totalByteCount: ext.totalByteCount, stream }; yield* stream(false); } @@ -355,43 +361,55 @@ async function inlineJsonAsArrow( } } -function isDownloadFailure(err: unknown): err is ExecutionError { - return ( - err instanceof ExecutionError && - err.errorCode === EXTERNAL_LINK_DOWNLOAD_FAILED - ); -} - -/** Storage answered 400 or 403 even after the link re-mint. */ -function isRefusal(err: ExecutionError): boolean { - const status = err.context?.httpStatus; - return status === 400 || status === 403; -} - -/** Download refused (or unanswered) and the result cannot be sent inline. */ -function linksDenied(cause: ExecutionError): ExecutionError { +/** Download refused or unanswered, and the result cannot be sent inline. */ +function linksDenied(cause: unknown): ExecutionError { return new ExecutionError( - `Cloud Fetch download failed and the result is too large to send inline: ${cause.message}`, + `Cloud Fetch download failed and the result is too large to send inline: ${errMessage(cause)}`, { errorCode: "EXTERNAL_LINKS_DENIED", - cause, + cause: cause instanceof Error ? cause : undefined, clientMessage: - "The query result could not be downloaded from workspace storage and is too large to send inline. Check the storage account's firewall and the app's network policy.", + "The query result could not be downloaded from workspace storage and is too large to send inline. Check the workspace storage firewall and the app's network policy.", }, ); } +type Download = + | { stream: AsyncGenerator } + | { error: unknown; kind: DownloadFailureKind }; + +/** Pull the first chunk, so a failed download surfaces before the route sends headers. */ +async function startDownload( + bytes: AsyncGenerator, + signal?: AbortSignal, +): Promise { + let first: IteratorResult; + try { + first = await bytes.next(); + } catch (error: unknown) { + const kind = signal?.aborted ? undefined : downloadFailureKind(error); + if (!kind) throw error; + return { error, kind }; + } + return { + stream: (async function* () { + if (!first.done) yield first.value; + yield* bytes; + })(), + }; +} + /** * {@link deliverArrowBytes}, falling back to an inline `JSON_ARRAY` run when the - * Cloud Fetch download fails before its first byte (pulled here, before the - * route sends headers). + * Cloud Fetch download fails before its first byte. * - * A 400/403 that survives the re-mint reports `"links-denied"` at once; a + * A refusal (400/403 after the re-mint) reports `"links-denied"` at once; a * download that never answers reports it after the fallback, since blocked * hosts hang instead of refusing. Other download errors fall back without being - * remembered. A result too large for inline gets one patient Cloud Fetch retry, - * or `EXTERNAL_LINKS_DENIED` after a refusal. With the `"links-denied"` hint, - * inline runs first. + * remembered. A result too large for inline gets one Cloud Fetch retry with the + * full header wait, and `EXTERNAL_LINKS_DENIED` only when storage refused or + * never answered. With the `"links-denied"` hint, inline runs first and only a + * result too large for it goes back to Cloud Fetch. */ export async function* deliverArrowBytesWithFallback( executor: QueryExecutor, @@ -402,88 +420,73 @@ export async function* deliverArrowBytesWithFallback( signal?: AbortSignal, opts?: ArrowDeliveryOptions, ): AsyncGenerator { + const inline = () => + inlineJsonAsArrow(executor, query, processedParams, out, signal); + const remember = (capability: ArrowCapability) => + opts?.onCapabilityResolved?.(capability); const hinted = opts?.capabilityHint === "links-denied"; let tooLarge = false; - let inlineError: unknown; if (hinted) { - const inline = await inlineJsonAsArrow( + const attempt = await inline(); + if ("bytes" in attempt) { + yield attempt.bytes; + return; + } + if (!(attempt.error instanceof InlineDoesNotFit)) throw attempt.error; + tooLarge = true; + } + + const download = await startDownload( + deliverArrowBytes( executor, + streamer, query, processedParams, out, signal, - ); - if ("bytes" in inline) { - yield inline.bytes; - return; - } - if (inline.error instanceof InlineDoesNotFit) tooLarge = true; - else inlineError = inline.error; - } - - const bytes = deliverArrowBytes( - executor, - streamer, - query, - processedParams, - out, + hinted ? { capabilityHint: "external" } : opts, + ), signal, - hinted ? { capabilityHint: "external" } : opts, ); - let first: IteratorResult; - let downloadError: ExecutionError | undefined; - try { - first = await bytes.next(); - } catch (err: unknown) { - if (signal?.aborted || !isDownloadFailure(err)) throw err; - downloadError = err; - first = { done: true, value: undefined }; - } - if (!downloadError) { - if (hinted) opts?.onCapabilityResolved?.("external"); - if (!first.done) yield first.value; - yield* bytes; + if ("stream" in download) { + if (hinted) remember("external"); + yield* download.stream; return; } - const refused = isRefusal(downloadError); - const unanswered = downloadError.context?.timedOut === true; - if (refused) opts?.onCapabilityResolved?.("links-denied"); + const { kind } = download; + if (kind === "refused") remember("links-denied"); const size = out.cloudFetch?.totalByteCount; tooLarge ||= size !== undefined && size > MAX_INLINE_ATTACHMENT_BYTES; - if (!tooLarge && inlineError === undefined) { - const inline = await inlineJsonAsArrow( - executor, - query, - processedParams, - out, - signal, + if (!tooLarge) { + logger.warn( + "Cloud Fetch download failed (%s), serving the result inline: %s", + kind, + errMessage(download.error), ); - if ("bytes" in inline) { - if (unanswered) opts?.onCapabilityResolved?.("links-denied"); - yield inline.bytes; + const attempt = await inline(); + if ("bytes" in attempt) { + if (kind === "unanswered") remember("links-denied"); + yield attempt.bytes; return; } - if (!(inline.error instanceof InlineDoesNotFit)) throw inline.error; - tooLarge = true; + if (!(attempt.error instanceof InlineDoesNotFit)) throw attempt.error; } - if (!tooLarge) throw inlineError; - if (refused || !out.cloudFetch) throw linksDenied(downloadError); + if (kind === "refused" || !out.cloudFetch) throw linksDenied(download.error); - const patient = out.cloudFetch.stream(true); - let retried: IteratorResult; - try { - retried = await patient.next(); - } catch (err: unknown) { - if (signal?.aborted || !isDownloadFailure(err)) throw err; - if (isRefusal(err) || err.context?.timedOut === true) { - opts?.onCapabilityResolved?.("links-denied"); - } - throw linksDenied(err); + logger.warn( + "Cloud Fetch download failed (%s) on a result too large for inline, retrying with the full header wait", + kind, + ); + const retry = await startDownload(out.cloudFetch.stream(true), signal); + if ("stream" in retry) { + if (hinted) remember("external"); + yield* retry.stream; + return; } - if (hinted) opts?.onCapabilityResolved?.("external"); - if (!retried.done) yield retried.value; - yield* patient; + const denied = retry.kind !== "other"; + if (denied || kind === "unanswered") remember("links-denied"); + throw denied ? linksDenied(retry.error) : retry.error; } /** JSON row result plus the metadata the SSE `result` message forwards. */ @@ -611,6 +614,13 @@ function renderNestedValue(value: unknown, type: DataType): unknown { } } +const NESTED_TYPES = new Set([ + Type.List, + Type.FixedSizeList, + Type.Struct, + Type.Map, +]); + /** Parse a STRING cell that looks like JSON into an object/array. */ function maybeParseJsonString(value: string): unknown { if (value && (value[0] === "{" || value[0] === "[")) { @@ -668,35 +678,12 @@ export function decodeArrowAttachmentToRows( const v = col?.get(i); if (v == null) { row[name] = null; - continue; - } - switch (type.typeId) { - case Type.Decimal: - case Type.Timestamp: - case Type.Date: - row[name] = formatJsonArrayCell(v, type); - continue; - // Nested columns (STRUCT / ARRAY / MAP) arrive on the native - // JSON_ARRAY wire as a JSON string with every scalar leaf stringified - // by type; render the same shape and stringify so callers cannot tell - // the fallback path from the native one. - case Type.List: - case Type.FixedSizeList: - case Type.Struct: - case Type.Map: - row[name] = JSON.stringify(renderNestedValue(v, type)); - continue; - } - if ( - typeof v === "number" || - typeof v === "bigint" || - typeof v === "boolean" - ) { - row[name] = String(v); + } else if (NESTED_TYPES.has(type.typeId)) { + // The native JSON_ARRAY wire sends nested columns as JSON text with + // every scalar leaf stringified by type. + row[name] = JSON.stringify(renderNestedValue(v, type)); } else if (typeof v === "string") { row[name] = maybeParseJsonString(v); - } else if (v instanceof Uint8Array) { - row[name] = Buffer.from(v).toString("base64"); } else { row[name] = formatJsonArrayCell(v, type); } diff --git a/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts b/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts index 07946951d..679d194ca 100644 --- a/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts @@ -497,8 +497,8 @@ describe("deliverArrowBytesWithFallback", () => { responseTimeoutMs: 100, }); const streamer: ArrowChunkStreamer = { - streamExternalLinks: (links, signal, refresh, patient) => - processor.streamChunks(links, signal, refresh, patient), + streamExternalLinks: (links, signal, refresh, fullHeaderWait) => + processor.streamChunks(links, signal, refresh, fullHeaderWait), }; return { data, storage, executions, executor, streamer }; } @@ -640,7 +640,7 @@ describe("deliverArrowBytesWithFallback", () => { expect(error?.errorCode).toBe("EXTERNAL_LINKS_DENIED"); expect(error?.clientMessage).toMatch(/workspace storage/); expect(error?.clientMessage).toMatch(/too large to send inline/); - expect(error?.clientMessage).not.toMatch(/azure/i); + expect(error?.clientMessage).not.toMatch(/azure|storage account/i); expect(ctx.executions).toEqual([ "INLINE+ARROW_STREAM", "EXTERNAL_LINKS+ARROW_STREAM", @@ -648,7 +648,7 @@ describe("deliverArrowBytesWithFallback", () => { expect(reported.at(-1)).toBe("links-denied"); }); - test("a slow first byte on a large result gets one patient pass instead of a denial", async () => { + test("a slow first byte on a large result gets one retry with the full header wait instead of a denial", async () => { const ctx = await setup({ storage: { kind: "ok", delayMs: 300 }, totalByteCount: LARGE, @@ -662,7 +662,7 @@ describe("deliverArrowBytesWithFallback", () => { expect(reported).not.toContain("links-denied"); }); - test("a large result whose storage never answers fails saying why after the patient pass", async () => { + test("a large result whose storage never answers fails saying why after the full-header-wait retry", async () => { const ctx = await setup({ storage: { kind: "hang" }, totalByteCount: LARGE, @@ -675,6 +675,26 @@ describe("deliverArrowBytesWithFallback", () => { expect(reported.at(-1)).toBe("links-denied"); }); + test.each([ + ["never answered", { kind: "hang" }, true], + ["reset after headers", { kind: "reset-after-headers" }, false], + ] as const)( + "a large result whose download %s and whose retry then fails otherwise keeps the download error", + async (_name, storage, remembered) => { + const ctx = await setup({ storage, totalByteCount: LARGE }); + const streamExternalLinks = ctx.streamer.streamExternalLinks; + ctx.streamer.streamExternalLinks = (links, signal, refresh, fullWait) => { + if (fullWait) ctx.storage.setMode({ kind: "status", status: 404 }); + return streamExternalLinks(links, signal, refresh, fullWait); + }; + const { error, reported } = await run(ctx); + + expect(error?.errorCode).toBe("EXTERNAL_LINK_DOWNLOAD_FAILED"); + expect(ctx.storage.gets()).toBe(2); + expect(reported.includes("links-denied")).toBe(remembered); + }, + ); + test("an inline result that spans several chunks is not served truncated", async () => { const ctx = await setup({ storage: { kind: "status", status: 403 }, @@ -726,7 +746,7 @@ describe("deliverArrowBytesWithFallback", () => { expect(error?.errorCode).toBe("EXTERNAL_LINKS_DENIED"); }); - test("with the links-denied hint, an unrelated inline failure is what the user sees when storage still refuses", async () => { + test("with the links-denied hint, an unrelated inline failure surfaces without running the query again", async () => { const ctx = await setup({ storage: { kind: "status", status: 403 }, inline: () => { @@ -739,6 +759,8 @@ describe("deliverArrowBytesWithFallback", () => { const { error } = await run(ctx, "links-denied"); expect(error?.errorCode).toBe("TEMPORARILY_UNAVAILABLE"); + expect(ctx.executions).toEqual(["INLINE+JSON_ARRAY"]); + expect(ctx.storage.gets()).toBe(0); }); test("a result the rebuild cannot parse fails with a code and a message, not a generic error", async () => { diff --git a/packages/appkit/src/stream/arrow-stream-processor.ts b/packages/appkit/src/stream/arrow-stream-processor.ts index 4c8cfd252..596282bda 100644 --- a/packages/appkit/src/stream/arrow-stream-processor.ts +++ b/packages/appkit/src/stream/arrow-stream-processor.ts @@ -18,7 +18,7 @@ export type RefreshChunkLink = ( ) => Promise; interface ArrowStreamOptions { - /** Idle timeout (ms) for a single read, and the response wait in patient mode. */ + /** Idle timeout (ms) for a single read, and the header wait with `fullHeaderWait`. */ timeout: number; /** Attempts to establish a chunk's response before any bytes are yielded. */ retries: number; @@ -27,10 +27,16 @@ interface ArrowStreamOptions { } /** `errorCode` on every failure to download a chunk, whether before or after the response headers. */ -export const EXTERNAL_LINK_DOWNLOAD_FAILED = "EXTERNAL_LINK_DOWNLOAD_FAILED"; +const EXTERNAL_LINK_DOWNLOAD_FAILED = "EXTERNAL_LINK_DOWNLOAD_FAILED"; const BACKOFF_MULTIPLIER = 1000; +interface DownloadFailure { + status?: number; + error: unknown; + timedOut: boolean; +} + type Connection = | { ok: true; @@ -38,21 +44,37 @@ type Connection = controller: AbortController; detach: () => void; } - | { ok: false; status?: number; error: unknown; timedOut: boolean }; + | ({ ok: false } & DownloadFailure); function downloadFailed( message: string, - httpStatus?: number, - cause?: unknown, - timedOut = false, + { status, error, timedOut }: DownloadFailure, ): ExecutionError { return new ExecutionError(message, { errorCode: EXTERNAL_LINK_DOWNLOAD_FAILED, - context: { httpStatus, timedOut }, - cause: cause instanceof Error ? cause : undefined, + context: { httpStatus: status, timedOut }, + cause: error instanceof Error ? error : undefined, }); } +/** Storage refused the download (400/403), never answered it, or it failed some other way. */ +export type DownloadFailureKind = "refused" | "unanswered" | "other"; + +/** The {@link DownloadFailureKind} of `err`, or `undefined` when it is not a chunk download failure. */ +export function downloadFailureKind( + err: unknown, +): DownloadFailureKind | undefined { + if ( + !(err instanceof ExecutionError) || + err.errorCode !== EXTERNAL_LINK_DOWNLOAD_FAILED + ) { + return undefined; + } + const status = err.context?.httpStatus; + if (status === 400 || status === 403) return "refused"; + return err.context?.timedOut === true ? "unanswered" : "other"; +} + /** * Streams Arrow IPC bytes for a completed statement's EXTERNAL_LINKS chunks * without ever buffering a whole chunk — each chunk's response body is piped @@ -85,22 +107,22 @@ export class ArrowStreamProcessor { * concatenation of everything yielded is byte-identical to the raw Arrow * result, so the client parses it exactly as a buffered response. * - * `patient` waits for response headers as long as the per-attempt `timeout` - * instead of `responseTimeoutMs`, for a second pass over a download that - * only looked stalled. + * `fullHeaderWait` waits for response headers as long as the per-attempt + * `timeout` instead of `responseTimeoutMs`, for a second pass over a + * download that only looked stalled. */ async *streamChunks( chunks: ExternalLink[], signal?: AbortSignal, refresh?: RefreshChunkLink, - patient = false, + fullHeaderWait = false, ): AsyncGenerator { if (chunks.length === 0) { throw ValidationError.missingField("chunks"); } for (const chunk of chunks) { - yield* this.streamChunkBody(chunk, signal, refresh, patient); + yield* this.streamChunkBody(chunk, signal, refresh, fullHeaderWait); } } @@ -120,7 +142,7 @@ export class ArrowStreamProcessor { chunk: ExternalLink, signal: AbortSignal | undefined, refresh: RefreshChunkLink | undefined, - patient: boolean, + fullHeaderWait: boolean, ): AsyncGenerator { let link = chunk; if (!link.externalLink) { @@ -131,11 +153,14 @@ export class ArrowStreamProcessor { } const { timeout, retries, responseTimeoutMs } = this.options; - const headerWaitMs = patient + const headerWaitMs = fullHeaderWait ? timeout : Math.min(responseTimeoutMs, timeout); let connection: Extract | undefined; - let failure: Extract | undefined; + let failure: DownloadFailure = { + error: new Error("no download attempts"), + timedOut: false, + }; let attempts = 0; let reminted = false; @@ -183,16 +208,14 @@ export class ArrowStreamProcessor { } if (!connection) { - const reason = failure?.timedOut + const reason = failure.timedOut ? `no response within ${headerWaitMs}ms` - : failure?.error instanceof Error + : failure.error instanceof Error ? failure.error.message - : String(failure?.error); + : String(failure.error); throw downloadFailed( `Failed to download chunk ${chunk.chunkIndex}: ${reason}`, - failure?.status, - failure?.error, - failure?.timedOut, + failure, ); } @@ -237,8 +260,7 @@ export class ArrowStreamProcessor { `Failed streaming chunk ${chunk.chunkIndex}: ${ error instanceof Error ? error.message : String(error) }`, - undefined, - error, + { error, timedOut: false }, ); } finally { clearTimeout(idleTimer); diff --git a/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts b/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts index 2d2caf623..fb06fb312 100644 --- a/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts +++ b/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts @@ -503,7 +503,7 @@ describe("ArrowStreamProcessor — storage that stalls", () => { expect(error.errorCode).toBe(EXTERNAL_LINK_DOWNLOAD_FAILED); }); - test("patient mode waits for headers as long as the attempt timeout", async () => { + test("fullHeaderWait waits for headers as long as the attempt timeout", async () => { handler = (_req, res) => { setTimeout(() => res.end(Buffer.from([9])), 300); }; From 9d177a5797ecc63fed5a84232d2dcbd39be0c80d Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 10 Nov 2026 12:19:41 +0000 Subject: [PATCH 9/9] refactor(appkit): drop the type casts the Cloud Fetch fallback added - Codec parsers name the Arrow class their table key guarantees (Int, Decimal, Timestamp) instead of casting to ad-hoc shapes, and the rebuild uses DataType.isDecimal. - A UsableLink guard replaces `externalLink as string`, and totalByteCount is converted with Number() instead of asserted. - The 4xx stop rule has a name, isFinalStatus. - Tests use real Response objects, instanceof checks, and `?? []` instead of casts that hid null, `as never`, and `as unknown as Response`. Signed-off-by: Pedro Figueiredo --- .../connectors/sql-warehouse/arrow-schema.ts | 6 +-- .../src/connectors/sql-warehouse/client.ts | 5 ++- .../sql-warehouse/json-array-cells.ts | 30 +++++++------ .../sql-warehouse/tests/arrow-schema.test.ts | 16 +++---- .../tests/json-array-cells.test.ts | 43 ++++++++++--------- .../analytics/tests/cloud-fetch-fixtures.ts | 6 +-- .../analytics/tests/result-delivery.test.ts | 7 +-- .../src/stream/arrow-stream-processor.ts | 29 +++++++------ .../tests/arrow-stream-processor.test.ts | 11 ++--- 9 files changed, 78 insertions(+), 75 deletions(-) diff --git a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts index bf4bc480b..82b88c96e 100644 --- a/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts +++ b/packages/appkit/src/connectors/sql-warehouse/arrow-schema.ts @@ -1,7 +1,7 @@ import { Binary, Bool, - type DataType, + DataType, DateDay, Decimal, DurationMicrosecond, @@ -129,8 +129,8 @@ function vectorFromCells(col: ColumnInfo, cells: Array) { const values = cells.map((cell) => cell === null ? null : parseJsonArrayCell(cell, type), ); - if (type.typeId === Type.Decimal) { - return decimalVector(values as Array, type as Decimal); + if (DataType.isDecimal(type)) { + return decimalVector(values as Array, type); } return vectorFromArray(values, type); } diff --git a/packages/appkit/src/connectors/sql-warehouse/client.ts b/packages/appkit/src/connectors/sql-warehouse/client.ts index 306224ff9..1c60a210b 100644 --- a/packages/appkit/src/connectors/sql-warehouse/client.ts +++ b/packages/appkit/src/connectors/sql-warehouse/client.ts @@ -1141,7 +1141,10 @@ export class SQLWarehouseConnector { error: response.status?.error, } as StatementStatus, columnNames: arrowColumnNames(response), - totalByteCount: response.manifest?.totalByteCount as number | undefined, + totalByteCount: + response.manifest?.totalByteCount === undefined + ? undefined + : Number(response.manifest.totalByteCount), // Resolve the pre-signed links for EVERY chunk in the caller's own // execution context. Streaming these directly (see // {@link streamExternalLinks}) avoids a second `getStatement` under diff --git a/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts b/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts index 7c61f0517..dc6728b76 100644 --- a/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts +++ b/packages/appkit/src/connectors/sql-warehouse/json-array-cells.ts @@ -1,4 +1,10 @@ -import { type DataType, Type } from "apache-arrow"; +import { + type DataType, + type Decimal, + type Int, + type Timestamp, + Type, +} from "apache-arrow"; /** * How one scalar type is written in an `INLINE` + `JSON_ARRAY` cell. `parse` @@ -40,13 +46,12 @@ function epochMillis(text: string, kind: string, cell: string): number { return ms; } -const hasTimezone = (type: DataType) => - (type as DataType & { timezone?: string | null }).timezone != null; +const hasTimezone = (type: Timestamp) => type.timezone != null; const CODECS: Partial> = { [Type.Int]: { - parse(cell, type) { - const { bitWidth } = type as DataType & { bitWidth: number }; + parse(cell, type: Int) { + const { bitWidth } = type; if (bitWidth === 64) { let n: bigint; try { @@ -77,8 +82,8 @@ const CODECS: Partial> = { }, }, [Type.Decimal]: { - format(value, type) { - const { scale } = type as DataType & { scale: number }; + format(value, type: Decimal) { + const { scale } = type; const unscaled = String(value); if (scale <= 0) return unscaled; const negative = unscaled.startsWith("-"); @@ -88,11 +93,8 @@ const CODECS: Partial> = { const out = `${digits.slice(0, point)}.${digits.slice(point)}`; return negative ? `-${out}` : out; }, - parse(cell, type) { - const { scale, precision } = type as DataType & { - scale: number; - precision: number; - }; + parse(cell, type: Decimal) { + const { scale, precision } = type; const match = DECIMAL_TEXT.exec(cell); if (!match) throw invalid("decimal", cell); const [, sign, whole, fraction = ""] = match; @@ -115,11 +117,11 @@ const CODECS: Partial> = { }, }, [Type.Timestamp]: { - format(value, type) { + format(value, type: Timestamp) { const iso = new Date(Number(value)).toISOString(); return hasTimezone(type) ? iso : iso.slice(0, -1); }, - parse(cell, type) { + parse(cell, type: Timestamp) { if (hasTimezone(type)) { if (!ZONED_TIMESTAMP_TEXT.test(cell)) throw invalid("timestamp", cell); return epochMillis(cell, "timestamp", cell); diff --git a/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts b/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts index 244138d7a..4eeb52517 100644 --- a/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts +++ b/packages/appkit/src/connectors/sql-warehouse/tests/arrow-schema.test.ts @@ -3,7 +3,7 @@ import { readFileSync } from "node:fs"; import { Binary, Bool, - type DataType, + DataType, DateDay, Decimal, DurationMicrosecond, @@ -532,8 +532,7 @@ describe("buildArrowIPCFromRows", () => { expected: unknown[]; }; - const decimalUnscaled = (v: unknown) => - v === null ? null : String(v as { toString(): string }); + const decimalUnscaled = (v: unknown) => (v === null ? null : String(v)); const cases: Record unknown }> = { INT: { column: { name: "c", typeName: "INT", typeText: "INT" }, @@ -622,7 +621,7 @@ describe("buildArrowIPCFromRows", () => { column: { name: "c", typeName: "BINARY", typeText: "BINARY" }, cells: ["AQI=", null], expected: [[1, 2], null], - read: (v) => (v === null ? null : [...(v as Uint8Array)]), + read: (v) => (v instanceof Uint8Array ? [...v] : v), }, VOID: { column: { name: "c", typeName: "NULL", typeText: "VOID" }, @@ -644,16 +643,15 @@ describe("buildArrowIPCFromRows", () => { const field = table.schema.fields[0]; expect(field.name).toBe(column.name); expect(field.typeId).toBe(native.typeId); - if (column.typeName === "DECIMAL") { - const type = field.type as Decimal; - expect([type.precision, type.scale]).toEqual([ + if (DataType.isDecimal(field.type)) { + expect([field.type.precision, field.type.scale]).toEqual([ column.typePrecision, column.typeScale, ]); } else { expect(String(field.type)).toBe(String(native)); } - const values = [...(table.getChildAt(0) as Iterable)]; + const values = [...(table.getChildAt(0) ?? [])]; expect(values.map((v) => (read ? read(v) : v))).toEqual(expected); }, ); @@ -802,7 +800,7 @@ describe("buildArrowIPCFromRows against a captured warehouse response", () => { expect(fixture.dataArray[0][index]).toMatch(/\.\d{3}-\d{2}:\d{2}$/); expect(values(rebuilt, index)).toEqual( values(native, index).map((ms) => - ms === null ? null : Math.trunc(ms as number), + ms === null ? null : Math.trunc(Number(ms)), ), ); }); diff --git a/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts b/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts index c05a84d7f..6e4910395 100644 --- a/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts +++ b/packages/appkit/src/connectors/sql-warehouse/tests/json-array-cells.test.ts @@ -125,11 +125,11 @@ describe("JSON_ARRAY cell codecs", () => { test.each(cases)( "%s: parsing the formatted text gives back the native Arrow value", (_name, build) => { - const column = tableFromIPC( + const table = tableFromIPC( tableToIPC(new Table({ c: build() }), "stream"), - ).getChildAt(0); - const type = column?.type as DataType; - for (const native of column ?? []) { + ); + const { type } = table.schema.fields[0]; + for (const native of table.getChildAt(0) ?? []) { const parsed = parseJsonArrayCell( formatJsonArrayCell(native, type), type, @@ -140,26 +140,27 @@ describe("JSON_ARRAY cell codecs", () => { ); test("formats match what a warehouse writes", () => { - const read = (vector: Vector) => - tableFromIPC(tableToIPC(new Table({ c: vector }), "stream")).getChildAt( - 0, + const formatted = (vector: Vector) => { + const table = tableFromIPC( + tableToIPC(new Table({ c: vector }), "stream"), ); - const decimal = read(decimalVector([-50n, 51n], 2, 10)); - const ntz = read( - vectorFromArray([Date.UTC(2026, 0, 1, 10)], new TimestampMicrosecond()), - ); - const day = read(vectorFromArray([Date.UTC(2026, 0, 1)], new DateDay())); + const { type } = table.schema.fields[0]; + return [...(table.getChildAt(0) ?? [])].map((v) => + formatJsonArrayCell(v, type), + ); + }; + expect(formatted(decimalVector([-50n, 51n], 2, 10))).toEqual([ + "-0.50", + "0.51", + ]); expect( - [...(decimal ?? [])].map((v) => - formatJsonArrayCell(v, decimal?.type as DataType), + formatted( + vectorFromArray([Date.UTC(2026, 0, 1, 10)], new TimestampMicrosecond()), ), - ).toEqual(["-0.50", "0.51"]); - expect(formatJsonArrayCell(ntz?.get(0), ntz?.type as DataType)).toBe( - "2026-01-01T10:00:00.000", - ); - expect(formatJsonArrayCell(day?.get(0), day?.type as DataType)).toBe( - "2026-01-01", - ); + ).toEqual(["2026-01-01T10:00:00.000"]); + expect( + formatted(vectorFromArray([Date.UTC(2026, 0, 1)], new DateDay())), + ).toEqual(["2026-01-01"]); }); test("a zoned TIMESTAMP keeps its instant when the warehouse writes the session offset", () => { diff --git a/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts index 233f487b1..7e32bc0eb 100644 --- a/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts +++ b/packages/appkit/src/plugins/analytics/tests/cloud-fetch-fixtures.ts @@ -93,8 +93,8 @@ export function typedDataset(rows = 20): Dataset { flag: vectorFromArray(flag, new Bool()), }); - const text = (v: unknown, render: (x: never) => string) => - v === null ? null : render(v as never); + const text = (v: T | null, render: (x: T) => string) => + v === null ? null : render(v); const dataArray = Array.from({ length: rows }, (_, i) => [ text(id[i], String), text(big[i], String), @@ -135,7 +135,7 @@ export function typedDataset(rows = 20): Dataset { /** Every cell as a string, so schemas and values compare across Arrow builds. */ export function cellsOf(table: Table): Array> { const columns = table.schema.fields.map((_f, index) => - [...(table.getChildAt(index) as Iterable)].map((v) => + [...(table.getChildAt(index) ?? [])].map((v) => v === null || v === undefined ? null : String(v), ), ); diff --git a/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts b/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts index 679d194ca..4360be4ec 100644 --- a/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/result-delivery.test.ts @@ -456,7 +456,7 @@ describe("deliverArrowBytesWithFallback", () => { options: { storage?: Parameters[0]; totalByteCount?: number; - inline?: () => unknown; + inline?: () => Awaited>; rows?: number; } = {}, ) { @@ -482,7 +482,7 @@ describe("deliverArrowBytesWithFallback", () => { external_links: [{ chunkIndex: 0, externalLink: storage.url() }], }; } - if (options.inline) return options.inline() as never; + if (options.inline) return options.inline(); return { statement_id: "stmt-json", columns: data.columns, @@ -527,7 +527,8 @@ describe("deliverArrowBytesWithFallback", () => { chunks.push(bytes); } } catch (e) { - error = e as ExecutionError; + if (!(e instanceof ExecutionError)) throw e; + error = e; } const bytes = Buffer.concat(chunks.map((c) => Buffer.from(c))); return { bytes, out, reported, error }; diff --git a/packages/appkit/src/stream/arrow-stream-processor.ts b/packages/appkit/src/stream/arrow-stream-processor.ts index 596282bda..d91c01020 100644 --- a/packages/appkit/src/stream/arrow-stream-processor.ts +++ b/packages/appkit/src/stream/arrow-stream-processor.ts @@ -31,6 +31,15 @@ const EXTERNAL_LINK_DOWNLOAD_FAILED = "EXTERNAL_LINK_DOWNLOAD_FAILED"; const BACKOFF_MULTIPLIER = 1000; +type UsableLink = ExternalLink & { externalLink: string }; + +const hasUrl = (link: ExternalLink | undefined): link is UsableLink => + Boolean(link?.externalLink); + +/** A 4xx won't change on retry, except a request timeout (408) or a rate limit (429). */ +const isFinalStatus = (status: number) => + status >= 400 && status < 500 && status !== 408 && status !== 429; + interface DownloadFailure { status?: number; error: unknown; @@ -144,13 +153,13 @@ export class ArrowStreamProcessor { refresh: RefreshChunkLink | undefined, fullHeaderWait: boolean, ): AsyncGenerator { - let link = chunk; - if (!link.externalLink) { + if (!hasUrl(chunk)) { // A missing link cannot be fixed by retrying — fail loudly. throw ExecutionError.statementFailed( `External link missing for chunk ${chunk.chunkIndex}`, ); } + let link: UsableLink = chunk; const { timeout, retries, responseTimeoutMs } = this.options; const headerWaitMs = fullHeaderWait @@ -182,7 +191,7 @@ export class ArrowStreamProcessor { attempts--; try { const fresh = await refresh(chunk.chunkIndex, signal); - if (fresh?.externalLink) link = fresh; + if (hasUrl(fresh)) link = fresh; } catch (refreshError) { // Retry the current URL; surface the original error if it fails. logger.warn( @@ -193,15 +202,7 @@ export class ArrowStreamProcessor { } continue; } - if ( - status !== undefined && - status >= 400 && - status < 500 && - status !== 408 && - status !== 429 - ) { - break; - } + if (status !== undefined && isFinalStatus(status)) break; if (attempts < retries) { await this.delay(2 ** (attempts - 1) * BACKOFF_MULTIPLIER, signal); } @@ -270,7 +271,7 @@ export class ArrowStreamProcessor { /** One GET; only the wait for response headers is timed. */ private async connect( - link: ExternalLink, + link: UsableLink, headerWaitMs: number, signal?: AbortSignal, ): Promise { @@ -280,7 +281,7 @@ export class ArrowStreamProcessor { const detach = () => signal?.removeEventListener("abort", onAbort); const timer = setTimeout(() => controller.abort(), headerWaitMs); try { - const response = await fetch(link.externalLink as string, { + const response = await fetch(link.externalLink, { signal: controller.signal, headers: link.httpHeaders, }); diff --git a/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts b/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts index fb06fb312..822b3946e 100644 --- a/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts +++ b/packages/appkit/src/stream/tests/arrow-stream-processor.test.ts @@ -272,19 +272,16 @@ describe("ArrowStreamProcessor.streamChunks", () => { }); const response = (status: number, statusText = "") => - ({ ok: false, status, statusText }) as Response; + new Response("denied", { status, statusText }); const okResponse = (...pieces: number[]) => - ({ - ok: true, - status: 200, - body: streamOf(new Uint8Array(pieces)), - }) as unknown as Response; + new Response(streamOf(new Uint8Array(pieces))); async function failureOf(gen: AsyncGenerator) { try { await drain(gen); } catch (error) { - return error as ExecutionError; + if (error instanceof ExecutionError) return error; + throw error; } throw new Error("expected the download to fail"); }