Skip to content

Commit e29e911

Browse files
FIR-16956: Add support for bytea (#39)
1 parent ed1e093 commit e29e911

File tree

6 files changed

+74
-3
lines changed

6 files changed

+74
-3
lines changed

.github/workflows/integration-tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,7 @@ jobs:
3333
FIREBOLT_ENGINE_NAME: ${{ steps.setup.outputs.engine_name }}
3434
FIREBOLT_ENGINE_ENDPOINT: ${{ steps.setup.outputs.engine_url }}
3535
FIREBOLT_API_ENDPOINT: ${{ secrets.FIREBOLT_API_ENDPOINT }}
36+
FIREBOLT_CLIENT_ID: ${{ secrets.FIREBOLT_CLIENT_ID }}
37+
FIREBOLT_CLIENT_SECRET: ${{ secrets.FIREBOLT_CLIENT_SECRET }}
3638
run: |
3739
npm run test:ci integration

src/formatter/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export class QueryFormatter {
136136
}
137137

138138
private escapeBuffer(param: Buffer) {
139-
return "X" + this.escapeString(param.toString("hex"));
139+
return this.escapeString("\\x" + param.toString("hex"));
140140
}
141141

142142
private escapeArray(param: unknown[], prefix = "[", suffix = "]") {

src/statement/dataTypes.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ const typeMapping = {
2727
uint8: "int",
2828
uint16: "int",
2929
uint32: "int",
30-
uint64: "long"
30+
uint64: "long",
31+
bytea: "bytea"
3132
};
3233

3334
const getMappedType = (innerType: string) => {
@@ -76,6 +77,8 @@ export const INTEGER_TYPES = withNullableTypes(["int", "integer", "long"]);
7677

7778
export const STRING_TYPES = withNullableTypes(["string", "text"]);
7879

80+
export const BYTEA_TYPES = withNullableTypes(["bytea"]);
81+
7982
export const getFireboltType = (type: string): string => {
8083
const key = type.toLowerCase();
8184
const match = key.match(COMPLEX_TYPE);
@@ -88,6 +91,10 @@ export const getFireboltType = (type: string): string => {
8891
return mappedType || key;
8992
};
9093

94+
export const isByteAType = (type: string) => {
95+
return BYTEA_TYPES.indexOf(type) !== -1;
96+
};
97+
9198
export const isDateType = (type: string) => {
9299
return DATE_TYPES.indexOf(type) !== -1 || type.match(/datetime64(.+)/i);
93100
};

src/statement/hydrateResponse.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import BigNumber from "bignumber.js";
22
import { ExecuteQueryOptions, Row } from "../types";
33
import { Meta } from "../meta";
4-
import { isDateType, isNumberType } from "./dataTypes";
4+
import { isByteAType, isDateType, isNumberType } from "./dataTypes";
55
import { hydrateDate } from "./hydrateDate";
66

77
const getHydratedValue = (
@@ -23,6 +23,10 @@ const getHydratedValue = (
2323
}
2424
return value;
2525
}
26+
if (isByteAType(type) && value != null) {
27+
const valueWithoutPrefix = (value as string).substring(2);
28+
return Buffer.from(valueWithoutPrefix.toString(), "hex");
29+
}
2630
return value;
2731
};
2832

test/integration/bytea.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Firebolt } from "../../src/index";
2+
3+
const connectionParams = {
4+
auth: {
5+
username: process.env.FIREBOLT_USERNAME as string,
6+
password: process.env.FIREBOLT_PASSWORD as string
7+
},
8+
database: process.env.FIREBOLT_DATABASE as string,
9+
engineName: process.env.FIREBOLT_ENGINE_NAME as string
10+
};
11+
12+
jest.setTimeout(100000);
13+
14+
describe("bytea", () => {
15+
it("handles select bytea", async () => {
16+
const firebolt = Firebolt({
17+
apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string
18+
});
19+
20+
const connection = await firebolt.connect(connectionParams);
21+
22+
const statement = await connection.execute(
23+
"SELECT 'hello_world_123ツ\n\u0048'::bytea"
24+
);
25+
26+
const { data, meta } = await statement.fetchResult();
27+
expect(meta[0].type).toEqual("bytea");
28+
const row = data[0];
29+
expect((row as unknown[])[0]).toEqual(
30+
Buffer.from("hello_world_123ツ\n\u0048")
31+
);
32+
});
33+
34+
it("handles select null bytea", async () => {
35+
const firebolt = Firebolt({
36+
apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string
37+
});
38+
39+
const connection = await firebolt.connect(connectionParams);
40+
41+
const statement = await connection.execute("SELECT null::bytea");
42+
43+
const { data, meta } = await statement.fetchResult();
44+
expect(meta[0].type).toEqual("nullable(bytea)");
45+
const row = data[0];
46+
expect((row as unknown[])[0]).toEqual(null);
47+
});
48+
});

test/unit/statement.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,14 @@ describe("format query", () => {
234234
`"select '123'::INT, 100, '2023-03-01'::DATE from table"`
235235
);
236236
});
237+
it("format bytea", () => {
238+
const queryFormatter = new QueryFormatter();
239+
const query = "SELECT 'hello_world'::bytea == ?";
240+
const buffer = Buffer.from("68656c6c6f5f776f726c64", "hex");
241+
const formattedQuery = queryFormatter.formatQuery(query, [buffer]);
242+
// Jest escaping rules are different, so we need to double the amount of quotes compared to .toEqual()
243+
expect(formattedQuery).toMatchInlineSnapshot(
244+
`"SELECT 'hello_world'::bytea == '\\\\\\\\x68656c6c6f5f776f726c64'"`
245+
);
246+
});
237247
});

0 commit comments

Comments
 (0)