Skip to content

Commit fd58b0e

Browse files
feat: Fir 29768 add support for set statements in node sdk (#83)
1 parent 55911a1 commit fd58b0e

File tree

5 files changed

+193
-5
lines changed

5 files changed

+193
-5
lines changed

src/connection/base.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const allowedUpdateParameters = ["database"];
2626
const updateEndpointHeader = "Firebolt-Update-Endpoint";
2727
const resetSessionHeader = "Firebolt-Reset-Session";
2828
const immutableParameters = ["database", "account_id", "output_format"];
29+
const testConnectionQuery = "SELECT 1";
2930

3031
export abstract class Connection {
3132
protected context: Context;
@@ -171,11 +172,21 @@ export abstract class Connection {
171172
};
172173

173174
const { parameters, namedParameters } = executeQueryOptions;
174-
const formattedQuery = queryFormatter.formatQuery(
175-
query,
176-
parameters,
177-
namedParameters
178-
);
175+
176+
let setKey = "",
177+
setValue = "",
178+
formattedQuery: string;
179+
if (queryFormatter.isSetStatement(query)) {
180+
[setKey, setValue] = queryFormatter.splitSetStatement(query);
181+
this.parameters[setKey] = setValue;
182+
formattedQuery = testConnectionQuery;
183+
} else {
184+
formattedQuery = queryFormatter.formatQuery(
185+
query,
186+
parameters,
187+
namedParameters
188+
);
189+
}
179190

180191
const body = formattedQuery;
181192
const url = this.getRequestUrl(executeQueryOptions);
@@ -197,6 +208,12 @@ export abstract class Connection {
197208
executeQueryOptions
198209
});
199210
return statement;
211+
} catch (error) {
212+
// In case it was a set query, remove set parameter if query fails
213+
if (setKey.length > 0) {
214+
delete this.parameters[setKey];
215+
}
216+
throw error;
200217
} finally {
201218
this.activeRequests.delete(request);
202219
}

src/formatter/index.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const CHARS_ESCAPE_MAP: Record<string, string> = {
1515
"'": "\\'",
1616
"\\": "\\\\"
1717
};
18+
const SET_PREFIX = "set ";
1819

1920
export class Tuple {
2021
value: unknown[];
@@ -238,4 +239,43 @@ export class QueryFormatter {
238239
}
239240
return query;
240241
}
242+
243+
isSetStatement(query: string): boolean {
244+
return query.trim().toLowerCase().startsWith(SET_PREFIX);
245+
}
246+
247+
trimTrailingSemicolon(text: string): string {
248+
let end = text.length;
249+
while (end > 0 && text[end - 1] == ";") {
250+
end--;
251+
}
252+
return text.substring(0, end);
253+
}
254+
255+
trimStringQuotes(text: string): string {
256+
if (text.length < 2) {
257+
return text;
258+
}
259+
260+
const firstChar = text[0];
261+
const lastChar = text[text.length - 1];
262+
if (firstChar === lastChar && (firstChar === "'" || firstChar === '"')) {
263+
return text.substring(1, text.length - 1);
264+
}
265+
266+
return text;
267+
}
268+
269+
splitSetStatement(query: string): [string, string] {
270+
query = this.trimTrailingSemicolon(query);
271+
272+
const strippedEquation = query.trim().substring(SET_PREFIX.length).trim();
273+
const index = strippedEquation.indexOf("=");
274+
const key = strippedEquation.slice(0, index),
275+
value = strippedEquation.slice(index + 1);
276+
if (key.length == 0 || value.length == 0) {
277+
throw new Error("Invalid SET statement format");
278+
}
279+
return [key.trim(), this.trimStringQuotes(value.trim())];
280+
}
241281
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Firebolt } from "../../../src/index";
2+
3+
const connectionParams = {
4+
auth: {
5+
client_id: process.env.FIREBOLT_CLIENT_ID as string,
6+
client_secret: process.env.FIREBOLT_CLIENT_SECRET as string
7+
},
8+
account: process.env.FIREBOLT_ACCOUNT_V1 as string,
9+
database: process.env.FIREBOLT_DATABASE as string,
10+
engineName: process.env.FIREBOLT_ENGINE_NAME as string
11+
};
12+
13+
jest.setTimeout(100000);
14+
15+
describe("set statements execution", () => {
16+
it("set statements are executed correctly", async () => {
17+
const setQuery = "SET time_zone=America/New_York";
18+
const query =
19+
"SELECT '2023-01-05 17:04:42.123456 Europe/Berlin'::TIMESTAMPTZ;";
20+
21+
const firebolt = Firebolt({
22+
apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string
23+
});
24+
25+
const connection = await firebolt.connect(connectionParams);
26+
27+
await connection.execute(setQuery);
28+
const statement = await connection.execute(query);
29+
30+
const { data } = await statement.fetchResult();
31+
const expected = new Date("2023-01-05T11:04:42.123456-05:00");
32+
33+
expect(data[0][0]).toEqual(expected);
34+
});
35+
});

test/unit/connection.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,4 +605,72 @@ describe("Connection", () => {
605605
await connection.execute("SELECT 1");
606606
expect(databaseUsed).toEqual("dummy");
607607
});
608+
609+
it("handles set statements correctly", async () => {
610+
let searchParamsUsed = new URLSearchParams();
611+
server.use(
612+
rest.post(`https://some_engine.com`, async (req, res, ctx) => {
613+
const body = await req.text();
614+
if (body.startsWith("SELECT 1")) {
615+
searchParamsUsed = req.url.searchParams;
616+
return res(ctx.json(selectOneResponse));
617+
}
618+
})
619+
);
620+
621+
const connectionParams: ConnectionOptions = {
622+
auth: {
623+
client_id: "dummy",
624+
client_secret: "dummy"
625+
},
626+
database: "dummy",
627+
engineName: "dummy",
628+
account: "my_account"
629+
};
630+
const firebolt = Firebolt({
631+
apiEndpoint
632+
});
633+
634+
const connection = await firebolt.connect(connectionParams);
635+
await connection.execute("SET param=value");
636+
await connection.execute("SELECT 1");
637+
expect(searchParamsUsed.get("param")).toEqual("value");
638+
});
639+
640+
it("handles invalid set statements correctly", async () => {
641+
let searchParamsUsed = new URLSearchParams();
642+
let searchParamsUsed2 = new URLSearchParams();
643+
server.use(
644+
rest.post(`https://some_engine.com`, async (req, res, ctx) => {
645+
const body = await req.text();
646+
if (body.startsWith("SELECT 1")) {
647+
searchParamsUsed = req.url.searchParams;
648+
return res(ctx.status(500));
649+
}
650+
if (body.startsWith("SELECT 2")) {
651+
searchParamsUsed2 = req.url.searchParams;
652+
return res(ctx.json(emptyResponse));
653+
}
654+
})
655+
);
656+
657+
const connectionParams: ConnectionOptions = {
658+
auth: {
659+
client_id: "dummy",
660+
client_secret: "dummy"
661+
},
662+
database: "dummy",
663+
engineName: "dummy",
664+
account: "my_account"
665+
};
666+
const firebolt = Firebolt({
667+
apiEndpoint
668+
});
669+
670+
const connection = await firebolt.connect(connectionParams);
671+
await expect(connection.execute("SET param=value")).rejects.toThrow();
672+
await connection.execute("SELECT 2");
673+
expect(searchParamsUsed.get("param")).toEqual("value");
674+
expect(searchParamsUsed2.get("param")).toEqual(null);
675+
});
608676
});

test/unit/statement.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,31 @@ describe("parse values", () => {
299299
expect(isNaN(res["nnan"])).toBe(true);
300300
});
301301
});
302+
303+
describe("set statements", () => {
304+
it.each([
305+
["select 1", false],
306+
["set foo = 1", true],
307+
[" set foo = 1", true],
308+
["update table set foo = 1", false]
309+
])("detects set statement %s correctly", (query, expected) => {
310+
expect(new QueryFormatter().isSetStatement(query)).toBe(expected);
311+
});
312+
313+
it.each([
314+
["set foo = bar", "foo", "bar"],
315+
[" set foo = bar ", "foo", "bar"],
316+
["\t\r set \t\nfoo \t\r = \t\n bar \r\n", "foo", "bar"],
317+
["set foo = bar;", "foo", "bar"],
318+
["set a='some 'string'", "a", "some 'string"],
319+
[
320+
'set query_parameters={"name":"param1","value":"Hello, world!"}',
321+
"query_parameters",
322+
'{"name":"param1","value":"Hello, world!"}'
323+
],
324+
["set key='val=ue'", "key", "val=ue"],
325+
["set key='val\nue'", "key", "val\nue"]
326+
])("parses set statement %j %j %j correctly", (query, key, value) => {
327+
expect(new QueryFormatter().splitSetStatement(query)).toEqual([key, value]);
328+
});
329+
});

0 commit comments

Comments
 (0)