Skip to content

Commit 796ce41

Browse files
fix: FIR-45354 Add null suffix to Firebolt types
1 parent 82a3b01 commit 796ce41

File tree

5 files changed

+110
-75
lines changed

5 files changed

+110
-75
lines changed

src/statement/dataTypes.ts

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,34 @@ const typeMapping = {
3434
bytea: "bytea"
3535
};
3636

37+
const COMPLEX_TYPE = /^(nullable|array)\((.+)\)( null)?/;
38+
const STRUCT_TYPE = /^(struct)\((.+)\)/;
39+
const NULLABLE_TYPE = /^(.+)( null)$/;
40+
const DATETIME_TYPE = /datetime64(.+)/i;
41+
const TIMESTAMP_TYPE = /timestamp_ext(.+)/i;
42+
const DECIMAL_TYPE = /decimal(.+)/i;
43+
const NUMERIC_TYPE = /numeric(.+)/i;
44+
3745
const getMappedType = (innerType: string) => {
38-
const type = typeMapping[innerType as keyof typeof typeMapping];
39-
if (type) {
40-
return type;
46+
const match = NULLABLE_TYPE.exec(innerType);
47+
const type = match ? match[1] : innerType;
48+
const nullableSuffix = match ? " null" : "";
49+
const mappedType = typeMapping[type as keyof typeof typeMapping];
50+
if (mappedType) {
51+
return `${mappedType}${nullableSuffix}`;
4152
}
42-
if (
43-
RegExp(/datetime64(.+)/i).exec(innerType) ||
44-
RegExp(/timestamp_ext(.+)/i).exec(innerType)
45-
) {
46-
return typeMapping.timestamp;
53+
const timestampPrecision =
54+
RegExp(DATETIME_TYPE).exec(type) || RegExp(TIMESTAMP_TYPE).exec(type);
55+
if (timestampPrecision) {
56+
return `${typeMapping.timestamp}${timestampPrecision[1]}${nullableSuffix}`;
4757
}
48-
if (
49-
RegExp(/decimal(.+)/i).exec(innerType) ||
50-
RegExp(/numeric(.+)/i).exec(innerType)
51-
) {
52-
return typeMapping.decimal;
58+
const decimalPrecision =
59+
RegExp(DECIMAL_TYPE).exec(type) || RegExp(NUMERIC_TYPE).exec(type);
60+
if (decimalPrecision) {
61+
return `${typeMapping.decimal}${decimalPrecision[1]}${nullableSuffix}`;
5362
}
5463
};
5564

56-
const COMPLEX_TYPE = /^(nullable|array)\((.+)\)/;
57-
const STRUCT_TYPE = /^(struct)\((.+)\)/;
58-
5965
const DATE_TYPES = withNullableTypes([
6066
"pg_date",
6167
"pgdate",
@@ -89,20 +95,30 @@ export const STRING_TYPES = withNullableTypes(["string", "text"]);
8995

9096
export const BYTEA_TYPES = withNullableTypes(["bytea"]);
9197

92-
//todo fix nullable types FIR-45354
9398
export const getFireboltType = (type: string): string => {
9499
const key = type.toLowerCase();
95-
const match = key.match(COMPLEX_TYPE);
100+
const match = RegExp(COMPLEX_TYPE).exec(key);
101+
if (match) {
102+
const [_, outerType, innerType, nullable] = match;
103+
const fireboltType = getFireboltType(innerType);
104+
105+
return fireboltType
106+
? `${outerType}(${fireboltType})${nullable ?? ""}`
107+
: key;
108+
}
109+
const mappedType = getMappedType(key);
110+
return mappedType ?? key;
111+
};
112+
113+
export const getInnerType = (type: string): string => {
114+
const key = type.toLowerCase();
115+
const match = RegExp(COMPLEX_TYPE).exec(key);
96116
if (match) {
97117
const [_, outerType, innerType] = match;
98-
if (innerType.match(COMPLEX_TYPE)) {
99-
return getFireboltType(innerType);
100-
}
101-
const mappedType = getMappedType(innerType);
102-
return mappedType ? `${outerType}(${mappedType})` : key;
118+
return getFireboltType(innerType);
103119
}
104120
const mappedType = getMappedType(key);
105-
return mappedType || key;
121+
return mappedType ?? key;
106122
};
107123

108124
const trimElement = (element: string) =>
@@ -142,31 +158,18 @@ export const getStructTypes = (type: string): Record<string, string> => {
142158
return {};
143159
};
144160

145-
export const getInnerType = (type: string): string => {
146-
const key = type.toLowerCase();
147-
const match = key.match(COMPLEX_TYPE);
148-
if (match) {
149-
const [_, _outerType, innerType] = match;
150-
if (innerType.match(COMPLEX_TYPE)) {
151-
return getInnerType(innerType);
152-
}
153-
const mappedType = getMappedType(innerType);
154-
return mappedType || innerType;
155-
}
156-
const mappedType = getMappedType(key);
157-
return mappedType || key;
158-
};
159-
160161
export const isByteAType = (type: string) => {
161162
return BYTEA_TYPES.indexOf(type) !== -1;
162163
};
163164

164165
export const isDateType = (type: string) => {
165-
return DATE_TYPES.indexOf(type) !== -1 || type.match(/datetime64(.+)/i);
166+
return (
167+
DATE_TYPES.indexOf(type) !== -1 || RegExp(/datetime64(.+)/i).exec(type)
168+
);
166169
};
167170

168171
export const isFloatType = (type: string) => {
169-
return FLOAT_TYPES.includes(type) || type.match(/decimal(.+)/i);
172+
return FLOAT_TYPES.includes(type) || RegExp(/decimal(.+)/i).exec(type);
170173
};
171174

172175
export const isNumberType = (type: string) => {

src/statement/hydrateResponse.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {
55
isByteAType,
66
isDateType,
77
isNumberType,
8-
getInnerType,
98
getStructTypes,
10-
isStructType
9+
isStructType,
10+
getInnerType
1111
} from "./dataTypes";
1212
import { hydrateDate } from "./hydrateDate";
1313

test/integration/v2/fetchTypes.test.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,15 @@ describe("test type casting on fetch", () => {
189189
" true as col_boolean,\n" +
190190
" null::bool as col_boolean_null,\n" +
191191
" [1,2,3,4] as col_array,\n" +
192-
// " null::array(int) as col_array_null,\n" +
192+
" null::array(int) as col_array_null,\n" +
193193
" '1231232.123459999990457054844258706536'::decimal(38, 30) as col_decimal,\n" +
194-
// " null::decimal(38, 30) as col_decimal_null,\n" +
194+
" null::decimal(38, 30) as col_decimal_null,\n" +
195195
" 'abc123'::bytea as col_bytea,\n" +
196196
" null::bytea as col_bytea_null,\n" +
197197
" 'point(1 2)'::geography as col_geography,\n" +
198-
" null::geography as col_geography_null,"
198+
" null::geography as col_geography_null,\n" +
199+
" [[1,2],[null,2],null]::array(array(int)) as col_arr_arr,\n" +
200+
" null::array(array(int)) as col_arr_arr_null"
199201
);
200202
const { data, meta } = await statement.fetchResult();
201203
const metaObjects = [
@@ -218,14 +220,15 @@ describe("test type casting on fetch", () => {
218220
{ name: "col_boolean", type: "boolean" },
219221
{ name: "col_boolean_null", type: "boolean null" },
220222
{ name: "col_array", type: "array(int)" },
221-
// { name: "col_array_null", type: "array(int) null" },
222-
// { name: "col_decimal", type: "decimal(38, 30)" },
223-
{ name: "col_decimal", type: "decimal" },
224-
// { name: "col_decimal_null", type: "decimal(38, 30) null" },
223+
{ name: "col_array_null", type: "array(int) null" },
224+
{ name: "col_decimal", type: "decimal(38, 30)" },
225+
{ name: "col_decimal_null", type: "decimal(38, 30) null" },
225226
{ name: "col_bytea", type: "bytea" },
226227
{ name: "col_bytea_null", type: "bytea null" },
227228
{ name: "col_geography", type: "geography" },
228-
{ name: "col_geography_null", type: "geography null" }
229+
{ name: "col_geography_null", type: "geography null" },
230+
{ name: "col_arr_arr", type: "array(array(int null) null)" },
231+
{ name: "col_arr_arr_null", type: "array(array(int)) null" }
229232
];
230233
for (let i = 0; i < meta.length; i++) {
231234
expect(meta[i]).toEqual(metaObjects[i]);
@@ -261,29 +264,27 @@ describe("test type casting on fetch", () => {
261264
" true as col_boolean,\n" +
262265
" null::bool as col_boolean_null,\n" +
263266
" [1,2,3,4] as col_array,\n" +
264-
// " null::array(int) as col_array_null,\n" +
267+
" null::array(int) as col_array_null,\n" +
265268
" '1231232.123459999990457054844258706536'::decimal(38, 30) as col_decimal,\n" +
266-
// " null::decimal(38, 30) as col_decimal_null,\n" +
269+
" null::decimal(38, 30) as col_decimal_null,\n" +
267270
" 'abc123'::bytea as col_bytea,\n" +
268271
" null::bytea as col_bytea_null,\n" +
269272
" 'point(1 2)'::geography as col_geography,\n" +
270-
" null::geography as col_geography_null,"
273+
" null::geography as col_geography_null,\n" +
274+
" [[1,2],[null,2],null]::array(array(int)) as col_arr_arr,\n" +
275+
" null::array(array(int)) as col_arr_arr_null"
271276
);
272277
const { data } = await statement.streamResult();
273278
const [meta] = await stream.once(data, "meta");
274279
const metaObjects = [
275280
{ name: "col_int", type: "int" },
276-
// { name: "col_int_null", type: "int null" },
277-
{ name: "col_int_null", type: "integer null" },
281+
{ name: "col_int_null", type: "int null" },
278282
{ name: "col_long", type: "long" },
279-
// { name: "col_long_null", type: "long null" },
280-
{ name: "col_long_null", type: "bigint null" },
283+
{ name: "col_long_null", type: "long null" },
281284
{ name: "col_float", type: "float" },
282-
// { name: "col_float_null", type: "float null" },
283-
{ name: "col_float_null", type: "real null" },
285+
{ name: "col_float_null", type: "float null" },
284286
{ name: "col_double", type: "double" },
285-
// { name: "col_double_null", type: "double null" },
286-
{ name: "col_double_null", type: "double precision null" },
287+
{ name: "col_double_null", type: "double null" },
287288
{ name: "col_text", type: "text" },
288289
{ name: "col_text_null", type: "text null" },
289290
{ name: "col_date", type: "date" },
@@ -295,14 +296,15 @@ describe("test type casting on fetch", () => {
295296
{ name: "col_boolean", type: "boolean" },
296297
{ name: "col_boolean_null", type: "boolean null" },
297298
{ name: "col_array", type: "array(int)" },
298-
// { name: "col_array_null", type: "array(int) null" },
299-
// { name: "col_decimal", type: "decimal(38, 30)" },
300-
{ name: "col_decimal", type: "decimal" },
301-
// { name: "col_decimal_null", type: "decimal(38, 30) null" },
299+
{ name: "col_array_null", type: "array(int) null" },
300+
{ name: "col_decimal", type: "decimal(38, 30)" },
301+
{ name: "col_decimal_null", type: "decimal(38, 30) null" },
302302
{ name: "col_bytea", type: "bytea" },
303303
{ name: "col_bytea_null", type: "bytea null" },
304304
{ name: "col_geography", type: "geography" },
305-
{ name: "col_geography_null", type: "geography null" }
305+
{ name: "col_geography_null", type: "geography null" },
306+
{ name: "col_arr_arr", type: "array(array(int null) null)" },
307+
{ name: "col_arr_arr_null", type: "array(array(int)) null" }
306308
];
307309
for (let i = 0; i < meta.length; i++) {
308310
expect(meta[i]).toEqual(metaObjects[i]);

test/integration/v2/preparedStatement.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe("prepared statement", () => {
112112
expect(meta.map((m: any) => m.type)).toEqual([
113113
"int", // $1
114114
"long", // $2
115-
"decimal", // $3
115+
"decimal(38, 4)", // $3
116116
"float", // $4
117117
"double", // $5
118118
"array(int null)", // $6
@@ -286,7 +286,7 @@ describe("prepared statement", () => {
286286
expect(meta.map((m: any) => m.type)).toEqual([
287287
"int", // $1
288288
"long", // $2
289-
"decimal", // $3
289+
"decimal(38, 4)", // $3
290290
"float", // $4
291291
"double", // $5
292292
"array(int null)", // $6

test/unit/hydrate.test.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
1-
import { getInnerType } from "../../src/statement/dataTypes";
1+
import { getFireboltType, getInnerType } from "../../src/statement/dataTypes";
22
import { getStructTypes } from "../../src/statement/dataTypes";
33

4-
describe("getInnerType function", () => {
4+
describe("getFireboltType function", () => {
5+
it("should return the firebolt type for a nullable type", () => {
6+
const type = "nullable(int)";
7+
const result = getFireboltType(type);
8+
expect(result).toBe("nullable(int)");
9+
});
10+
11+
it("should return the firebolt type for an array type", () => {
12+
const type = "array(int)";
13+
const result = getFireboltType(type);
14+
expect(result).toBe("array(int)");
15+
});
16+
17+
it("should return the firebolt type for a nullable array type", () => {
18+
const type = "array(int null) null";
19+
const result = getFireboltType(type);
20+
expect(result).toBe("array(int null) null");
21+
});
22+
23+
it("should return the firebolt type for a nullable array integer type", () => {
24+
const type = "array(integer null) null";
25+
const result = getFireboltType(type);
26+
expect(result).toBe("array(int null) null");
27+
});
28+
529
it("should return the inner type for a nullable type", () => {
630
const type = "nullable(int)";
731
const result = getInnerType(type);
@@ -14,15 +38,21 @@ describe("getInnerType function", () => {
1438
expect(result).toBe("int");
1539
});
1640

17-
it("should return the inner type for a nested nullable type", () => {
18-
const type = "nullable(nullable(int))";
41+
it("should return the inner type for a nullable array type", () => {
42+
const type = "array(int null) null";
1943
const result = getInnerType(type);
20-
expect(result).toBe("int");
44+
expect(result).toBe("int null");
45+
});
46+
47+
it("should return the inner type for a nullable array integer type", () => {
48+
const type = "array(integer null) null";
49+
const result = getInnerType(type);
50+
expect(result).toBe("int null");
2151
});
2252

2353
it("should return the original type if no complex type is found", () => {
2454
const type = "int";
25-
const result = getInnerType(type);
55+
const result = getFireboltType(type);
2656
expect(result).toBe("int");
2757
});
2858
});

0 commit comments

Comments
 (0)