Skip to content

Commit b47b841

Browse files
fix: FIR-45354 Add null suffix to Firebolt types (#141)
1 parent 10693ea commit b47b841

File tree

4 files changed

+102
-72
lines changed

4 files changed

+102
-72
lines changed

src/statement/dataTypes.ts

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,31 @@ 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+
if (RegExp(DATETIME_TYPE).exec(type) || RegExp(TIMESTAMP_TYPE).exec(type)) {
54+
return `${typeMapping.timestamp}${nullableSuffix}`;
4755
}
48-
if (
49-
RegExp(/decimal(.+)/i).exec(innerType) ||
50-
RegExp(/numeric(.+)/i).exec(innerType)
51-
) {
52-
return typeMapping.decimal;
56+
if (RegExp(DECIMAL_TYPE).exec(type) || RegExp(NUMERIC_TYPE).exec(type)) {
57+
return `${typeMapping.decimal}${nullableSuffix}`;
5358
}
59+
return null;
5460
};
5561

56-
const COMPLEX_TYPE = /^(nullable|array)\((.+)\)/;
57-
const STRUCT_TYPE = /^(struct)\((.+)\)/;
58-
5962
const DATE_TYPES = withNullableTypes([
6063
"pg_date",
6164
"pgdate",
@@ -89,20 +92,30 @@ export const STRING_TYPES = withNullableTypes(["string", "text"]);
8992

9093
export const BYTEA_TYPES = withNullableTypes(["bytea"]);
9194

92-
//todo fix nullable types FIR-45354
9395
export const getFireboltType = (type: string): string => {
9496
const key = type.toLowerCase();
95-
const match = key.match(COMPLEX_TYPE);
97+
const match = RegExp(COMPLEX_TYPE).exec(key);
9698
if (match) {
97-
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;
99+
const [, outerType, innerType, nullable] = match;
100+
const fireboltType = getFireboltType(innerType);
101+
102+
return fireboltType
103+
? `${outerType}(${fireboltType})${nullable ?? ""}`
104+
: key;
105+
}
106+
const mappedType = getMappedType(key);
107+
return mappedType ?? key;
108+
};
109+
110+
export const getInnerType = (type: string): string => {
111+
const key = type.toLowerCase();
112+
const match = RegExp(COMPLEX_TYPE).exec(key);
113+
if (match) {
114+
const [, , innerType] = match;
115+
return getFireboltType(innerType);
103116
}
104117
const mappedType = getMappedType(key);
105-
return mappedType || key;
118+
return mappedType ?? key;
106119
};
107120

108121
const trimElement = (element: string) =>
@@ -142,31 +155,16 @@ export const getStructTypes = (type: string): Record<string, string> => {
142155
return {};
143156
};
144157

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-
160158
export const isByteAType = (type: string) => {
161159
return BYTEA_TYPES.indexOf(type) !== -1;
162160
};
163161

164162
export const isDateType = (type: string) => {
165-
return DATE_TYPES.indexOf(type) !== -1 || type.match(/datetime64(.+)/i);
163+
return DATE_TYPES.indexOf(type) !== -1 || RegExp(DATETIME_TYPE).exec(type);
166164
};
167165

168166
export const isFloatType = (type: string) => {
169-
return FLOAT_TYPES.includes(type) || type.match(/decimal(.+)/i);
167+
return FLOAT_TYPES.includes(type) || RegExp(DECIMAL_TYPE).exec(type);
170168
};
171169

172170
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: 24 additions & 22 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_array_null", type: "array(int) null" },
223224
{ name: "col_decimal", type: "decimal" },
224-
// { name: "col_decimal_null", type: "decimal(38, 30) null" },
225+
{ name: "col_decimal_null", type: "decimal 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)" },
299+
{ name: "col_array_null", type: "array(int) null" },
300300
{ name: "col_decimal", type: "decimal" },
301-
// { name: "col_decimal_null", type: "decimal(38, 30) null" },
301+
{ name: "col_decimal_null", type: "decimal 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/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)