From 034c39e02ebcbb7350089af620972dc62380480c Mon Sep 17 00:00:00 2001 From: Bogdan Truta Date: Wed, 14 Jan 2026 13:20:05 +0200 Subject: [PATCH] fix: FIR-51861 null pointer when hydrating null struct --- src/statement/hydrateResponse.ts | 3 ++ test/integration/v2/nullStruct.test.ts | 53 ++++++++++++++++++++++++++ test/unit/v2/statement.test.ts | 20 ++++++++++ 3 files changed, 76 insertions(+) create mode 100644 test/integration/v2/nullStruct.test.ts diff --git a/src/statement/hydrateResponse.ts b/src/statement/hydrateResponse.ts index 7fac3bff..c55dfd46 100644 --- a/src/statement/hydrateResponse.ts +++ b/src/statement/hydrateResponse.ts @@ -28,6 +28,9 @@ const hydrateStruct = ( type: string, executeQueryOptions: ExecuteQueryOptions ): Record => { + if (!value) { + return value; + } const hydratedStruct: Record = {}; const innerTypes = getStructTypes(type); diff --git a/test/integration/v2/nullStruct.test.ts b/test/integration/v2/nullStruct.test.ts new file mode 100644 index 00000000..1c2c815d --- /dev/null +++ b/test/integration/v2/nullStruct.test.ts @@ -0,0 +1,53 @@ +import { Firebolt } from "../../../src/index"; + +const connectionParams = { + auth: { + client_id: process.env.FIREBOLT_CLIENT_ID as string, + client_secret: process.env.FIREBOLT_CLIENT_SECRET as string + }, + account: process.env.FIREBOLT_ACCOUNT as string, + database: process.env.FIREBOLT_DATABASE as string, + engineName: process.env.FIREBOLT_ENGINE_NAME as string +}; + +jest.setTimeout(500000); + +describe("null struct integration tests", () => { + let firebolt: any; + let connection: any; + + beforeAll(async () => { + firebolt = Firebolt({ + apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string + }); + connection = await firebolt.connect(connectionParams); + }); + + it("should handle SELECT with null struct value (with normalization)", async () => { + const query = `SELECT NULL::struct(a int, b text) as s`; + + const statement = await connection.execute(query, { + response: { normalizeData: true } + }); + const { data, meta } = await statement.fetchResult(); + + expect(data.length).toBe(1); + expect(data[0].s).toBeNull(); + expect(meta[0].name).toBe("s"); + expect(meta[0].type).toContain("struct"); + }); + + it("should handle SELECT with null struct value (without normalization)", async () => { + const query = `SELECT NULL::struct(a int, b text) as s, 'value' as col2`; + + const statement = await connection.execute(query, { + response: { normalizeData: false } + }); + const { data } = await statement.fetchResult(); + + expect(data.length).toBe(1); + expect(Array.isArray(data[0])).toBe(true); + expect(data[0][0]).toBeNull(); + expect(data[0][1]).toBe("value"); + }); +}); diff --git a/test/unit/v2/statement.test.ts b/test/unit/v2/statement.test.ts index 7659dece..7050452a 100644 --- a/test/unit/v2/statement.test.ts +++ b/test/unit/v2/statement.test.ts @@ -433,6 +433,26 @@ describe("parse values", () => { } }); }); + + it("handles null struct value with normalization", () => { + const row = { + s: null + }; + const meta = [{ name: "s", type: "struct(a int, b text) null" }]; + const res: Record = hydrateRow(row, meta, {}); + expect(res["s"]).toBeNull(); + }); + + it("handles null struct value without normalization (array format)", () => { + const row = [null, "value2"]; + const meta = [ + { name: "s", type: "struct(a int, b text) null" }, + { name: "col2", type: "text" } + ]; + const res = hydrateRow(row, meta, {}) as unknown[]; + expect(res[0]).toBeNull(); + expect(res[1]).toBe("value2"); + }); }); describe("set statements", () => {