diff --git a/src/statement/dataTypes.ts b/src/statement/dataTypes.ts index 71abe89d..7d97e4e2 100644 --- a/src/statement/dataTypes.ts +++ b/src/statement/dataTypes.ts @@ -34,28 +34,31 @@ const typeMapping = { bytea: "bytea" }; +const COMPLEX_TYPE = /^(nullable|array)\((.+)\)( null)?/; +const STRUCT_TYPE = /^(struct)\((.+)\)/; +const NULLABLE_TYPE = /^(.+)( null)$/; +const DATETIME_TYPE = /datetime64(.+)/i; +const TIMESTAMP_TYPE = /timestamp_ext(.+)/i; +const DECIMAL_TYPE = /decimal(.+)/i; +const NUMERIC_TYPE = /numeric(.+)/i; + const getMappedType = (innerType: string) => { - const type = typeMapping[innerType as keyof typeof typeMapping]; - if (type) { - return type; + const match = NULLABLE_TYPE.exec(innerType); + const type = match ? match[1] : innerType; + const nullableSuffix = match ? " null" : ""; + const mappedType = typeMapping[type as keyof typeof typeMapping]; + if (mappedType) { + return `${mappedType}${nullableSuffix}`; } - if ( - RegExp(/datetime64(.+)/i).exec(innerType) || - RegExp(/timestamp_ext(.+)/i).exec(innerType) - ) { - return typeMapping.timestamp; + if (RegExp(DATETIME_TYPE).exec(type) || RegExp(TIMESTAMP_TYPE).exec(type)) { + return `${typeMapping.timestamp}${nullableSuffix}`; } - if ( - RegExp(/decimal(.+)/i).exec(innerType) || - RegExp(/numeric(.+)/i).exec(innerType) - ) { - return typeMapping.decimal; + if (RegExp(DECIMAL_TYPE).exec(type) || RegExp(NUMERIC_TYPE).exec(type)) { + return `${typeMapping.decimal}${nullableSuffix}`; } + return null; }; -const COMPLEX_TYPE = /^(nullable|array)\((.+)\)/; -const STRUCT_TYPE = /^(struct)\((.+)\)/; - const DATE_TYPES = withNullableTypes([ "pg_date", "pgdate", @@ -89,20 +92,30 @@ export const STRING_TYPES = withNullableTypes(["string", "text"]); export const BYTEA_TYPES = withNullableTypes(["bytea"]); -//todo fix nullable types FIR-45354 export const getFireboltType = (type: string): string => { const key = type.toLowerCase(); - const match = key.match(COMPLEX_TYPE); + const match = RegExp(COMPLEX_TYPE).exec(key); if (match) { - const [_, outerType, innerType] = match; - if (innerType.match(COMPLEX_TYPE)) { - return getFireboltType(innerType); - } - const mappedType = getMappedType(innerType); - return mappedType ? `${outerType}(${mappedType})` : key; + const [, outerType, innerType, nullable] = match; + const fireboltType = getFireboltType(innerType); + + return fireboltType + ? `${outerType}(${fireboltType})${nullable ?? ""}` + : key; + } + const mappedType = getMappedType(key); + return mappedType ?? key; +}; + +export const getInnerType = (type: string): string => { + const key = type.toLowerCase(); + const match = RegExp(COMPLEX_TYPE).exec(key); + if (match) { + const [, , innerType] = match; + return getFireboltType(innerType); } const mappedType = getMappedType(key); - return mappedType || key; + return mappedType ?? key; }; const trimElement = (element: string) => @@ -142,31 +155,16 @@ export const getStructTypes = (type: string): Record => { return {}; }; -export const getInnerType = (type: string): string => { - const key = type.toLowerCase(); - const match = key.match(COMPLEX_TYPE); - if (match) { - const [_, _outerType, innerType] = match; - if (innerType.match(COMPLEX_TYPE)) { - return getInnerType(innerType); - } - const mappedType = getMappedType(innerType); - return mappedType || innerType; - } - const mappedType = getMappedType(key); - return mappedType || key; -}; - export const isByteAType = (type: string) => { return BYTEA_TYPES.indexOf(type) !== -1; }; export const isDateType = (type: string) => { - return DATE_TYPES.indexOf(type) !== -1 || type.match(/datetime64(.+)/i); + return DATE_TYPES.indexOf(type) !== -1 || RegExp(DATETIME_TYPE).exec(type); }; export const isFloatType = (type: string) => { - return FLOAT_TYPES.includes(type) || type.match(/decimal(.+)/i); + return FLOAT_TYPES.includes(type) || RegExp(DECIMAL_TYPE).exec(type); }; export const isNumberType = (type: string) => { diff --git a/src/statement/hydrateResponse.ts b/src/statement/hydrateResponse.ts index 900c0f25..3de303e8 100644 --- a/src/statement/hydrateResponse.ts +++ b/src/statement/hydrateResponse.ts @@ -5,9 +5,9 @@ import { isByteAType, isDateType, isNumberType, - getInnerType, getStructTypes, - isStructType + isStructType, + getInnerType } from "./dataTypes"; import { hydrateDate } from "./hydrateDate"; diff --git a/test/integration/v2/fetchTypes.test.ts b/test/integration/v2/fetchTypes.test.ts index afa8f156..b165ce4e 100644 --- a/test/integration/v2/fetchTypes.test.ts +++ b/test/integration/v2/fetchTypes.test.ts @@ -189,13 +189,15 @@ describe("test type casting on fetch", () => { " true as col_boolean,\n" + " null::bool as col_boolean_null,\n" + " [1,2,3,4] as col_array,\n" + - // " null::array(int) as col_array_null,\n" + + " null::array(int) as col_array_null,\n" + " '1231232.123459999990457054844258706536'::decimal(38, 30) as col_decimal,\n" + - // " null::decimal(38, 30) as col_decimal_null,\n" + + " null::decimal(38, 30) as col_decimal_null,\n" + " 'abc123'::bytea as col_bytea,\n" + " null::bytea as col_bytea_null,\n" + " 'point(1 2)'::geography as col_geography,\n" + - " null::geography as col_geography_null," + " null::geography as col_geography_null,\n" + + " [[1,2],[null,2],null]::array(array(int)) as col_arr_arr,\n" + + " null::array(array(int)) as col_arr_arr_null" ); const { data, meta } = await statement.fetchResult(); const metaObjects = [ @@ -218,14 +220,15 @@ describe("test type casting on fetch", () => { { name: "col_boolean", type: "boolean" }, { name: "col_boolean_null", type: "boolean null" }, { name: "col_array", type: "array(int)" }, - // { name: "col_array_null", type: "array(int) null" }, - // { name: "col_decimal", type: "decimal(38, 30)" }, + { name: "col_array_null", type: "array(int) null" }, { name: "col_decimal", type: "decimal" }, - // { name: "col_decimal_null", type: "decimal(38, 30) null" }, + { name: "col_decimal_null", type: "decimal null" }, { name: "col_bytea", type: "bytea" }, { name: "col_bytea_null", type: "bytea null" }, { name: "col_geography", type: "geography" }, - { name: "col_geography_null", type: "geography null" } + { name: "col_geography_null", type: "geography null" }, + { name: "col_arr_arr", type: "array(array(int null) null)" }, + { name: "col_arr_arr_null", type: "array(array(int)) null" } ]; for (let i = 0; i < meta.length; i++) { expect(meta[i]).toEqual(metaObjects[i]); @@ -261,29 +264,27 @@ describe("test type casting on fetch", () => { " true as col_boolean,\n" + " null::bool as col_boolean_null,\n" + " [1,2,3,4] as col_array,\n" + - // " null::array(int) as col_array_null,\n" + + " null::array(int) as col_array_null,\n" + " '1231232.123459999990457054844258706536'::decimal(38, 30) as col_decimal,\n" + - // " null::decimal(38, 30) as col_decimal_null,\n" + + " null::decimal(38, 30) as col_decimal_null,\n" + " 'abc123'::bytea as col_bytea,\n" + " null::bytea as col_bytea_null,\n" + " 'point(1 2)'::geography as col_geography,\n" + - " null::geography as col_geography_null," + " null::geography as col_geography_null,\n" + + " [[1,2],[null,2],null]::array(array(int)) as col_arr_arr,\n" + + " null::array(array(int)) as col_arr_arr_null" ); const { data } = await statement.streamResult(); const [meta] = await stream.once(data, "meta"); const metaObjects = [ { name: "col_int", type: "int" }, - // { name: "col_int_null", type: "int null" }, - { name: "col_int_null", type: "integer null" }, + { name: "col_int_null", type: "int null" }, { name: "col_long", type: "long" }, - // { name: "col_long_null", type: "long null" }, - { name: "col_long_null", type: "bigint null" }, + { name: "col_long_null", type: "long null" }, { name: "col_float", type: "float" }, - // { name: "col_float_null", type: "float null" }, - { name: "col_float_null", type: "real null" }, + { name: "col_float_null", type: "float null" }, { name: "col_double", type: "double" }, - // { name: "col_double_null", type: "double null" }, - { name: "col_double_null", type: "double precision null" }, + { name: "col_double_null", type: "double null" }, { name: "col_text", type: "text" }, { name: "col_text_null", type: "text null" }, { name: "col_date", type: "date" }, @@ -295,14 +296,15 @@ describe("test type casting on fetch", () => { { name: "col_boolean", type: "boolean" }, { name: "col_boolean_null", type: "boolean null" }, { name: "col_array", type: "array(int)" }, - // { name: "col_array_null", type: "array(int) null" }, - // { name: "col_decimal", type: "decimal(38, 30)" }, + { name: "col_array_null", type: "array(int) null" }, { name: "col_decimal", type: "decimal" }, - // { name: "col_decimal_null", type: "decimal(38, 30) null" }, + { name: "col_decimal_null", type: "decimal null" }, { name: "col_bytea", type: "bytea" }, { name: "col_bytea_null", type: "bytea null" }, { name: "col_geography", type: "geography" }, - { name: "col_geography_null", type: "geography null" } + { name: "col_geography_null", type: "geography null" }, + { name: "col_arr_arr", type: "array(array(int null) null)" }, + { name: "col_arr_arr_null", type: "array(array(int)) null" } ]; for (let i = 0; i < meta.length; i++) { expect(meta[i]).toEqual(metaObjects[i]); diff --git a/test/unit/hydrate.test.ts b/test/unit/hydrate.test.ts index a50054e1..84c9274b 100644 --- a/test/unit/hydrate.test.ts +++ b/test/unit/hydrate.test.ts @@ -1,7 +1,31 @@ -import { getInnerType } from "../../src/statement/dataTypes"; +import { getFireboltType, getInnerType } from "../../src/statement/dataTypes"; import { getStructTypes } from "../../src/statement/dataTypes"; -describe("getInnerType function", () => { +describe("getFireboltType function", () => { + it("should return the firebolt type for a nullable type", () => { + const type = "nullable(int)"; + const result = getFireboltType(type); + expect(result).toBe("nullable(int)"); + }); + + it("should return the firebolt type for an array type", () => { + const type = "array(int)"; + const result = getFireboltType(type); + expect(result).toBe("array(int)"); + }); + + it("should return the firebolt type for a nullable array type", () => { + const type = "array(int null) null"; + const result = getFireboltType(type); + expect(result).toBe("array(int null) null"); + }); + + it("should return the firebolt type for a nullable array integer type", () => { + const type = "array(integer null) null"; + const result = getFireboltType(type); + expect(result).toBe("array(int null) null"); + }); + it("should return the inner type for a nullable type", () => { const type = "nullable(int)"; const result = getInnerType(type); @@ -14,15 +38,21 @@ describe("getInnerType function", () => { expect(result).toBe("int"); }); - it("should return the inner type for a nested nullable type", () => { - const type = "nullable(nullable(int))"; + it("should return the inner type for a nullable array type", () => { + const type = "array(int null) null"; const result = getInnerType(type); - expect(result).toBe("int"); + expect(result).toBe("int null"); + }); + + it("should return the inner type for a nullable array integer type", () => { + const type = "array(integer null) null"; + const result = getInnerType(type); + expect(result).toBe("int null"); }); it("should return the original type if no complex type is found", () => { const type = "int"; - const result = getInnerType(type); + const result = getFireboltType(type); expect(result).toBe("int"); }); });