Skip to content

Commit ca753db

Browse files
feat: Fir 30153 support use engine in node sdk (#78)
1 parent 19cc266 commit ca753db

23 files changed

+625
-132
lines changed

.github/workflows/integration-tests-v2.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ jobs:
2828
with:
2929
firebolt-client-id: ${{ secrets.FIREBOLT_CLIENT_ID_STG_NEW_IDN }}
3030
firebolt-client-secret: ${{ secrets.FIREBOLT_CLIENT_SECRET_STG_NEW_IDN }}
31-
account: ${{ vars.FIREBOLT_ACCOUNT }}
31+
account: ${{ vars.FIREBOLT_ACCOUNT_V1 }}
3232
api-endpoint: "api.staging.firebolt.io"
3333
region: "us-east-1"
3434

3535
- name: Run integration tests
3636
env:
37-
FIREBOLT_ACCOUNT: ${{ vars.FIREBOLT_ACCOUNT }}
37+
FIREBOLT_ACCOUNT_V1: ${{ vars.FIREBOLT_ACCOUNT_V1 }}
38+
FIREBOLT_ACCOUNT_V2: ${{ vars.FIREBOLT_ACCOUNT_V2 }}
3839
FIREBOLT_DATABASE: ${{ steps.setup.outputs.database_name }}
3940
FIREBOLT_ENGINE_NAME: ${{ steps.setup.outputs.engine_name }}
4041
FIREBOLT_API_ENDPOINT: "api.staging.firebolt.io"

src/connection/base.ts

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "../types";
77
import { Statement } from "../statement";
88
import { generateUserAgent } from "../common/util";
9+
import { ConnectionError } from "../common/errors";
910

1011
const defaultQuerySettings = {
1112
output_format: OutputFormat.COMPACT
@@ -15,19 +16,33 @@ const defaultResponseSettings = {
1516
normalizeData: false
1617
};
1718

19+
interface AccountInfo {
20+
id: string;
21+
infraVersion: number;
22+
}
23+
1824
const updateParametersHeader = "Firebolt-Update-Parameters";
1925
const allowedUpdateParameters = ["database"];
26+
const updateEndpointHeader = "Firebolt-Update-Endpoint";
27+
const resetSessionHeader = "Firebolt-Reset-Session";
28+
const immutableParameters = ["database", "account_id", "output_format"];
2029

2130
export abstract class Connection {
2231
protected context: Context;
2332
protected options: ConnectionOptions;
2433
protected userAgent: string;
34+
protected parameters: Record<string, string>;
35+
protected accountInfo: AccountInfo | undefined;
2536
engineEndpoint!: string;
2637
activeRequests = new Set<{ abort: () => void }>();
2738

2839
constructor(context: Context, options: ConnectionOptions) {
2940
this.context = context;
3041
this.options = options;
42+
this.parameters = {
43+
...(options.database ? { database: options.database } : {}),
44+
...defaultQuerySettings
45+
};
3146
this.userAgent = generateUserAgent(
3247
options.additionalParameters?.userClients,
3348
options.additionalParameters?.userDrivers
@@ -60,31 +75,82 @@ export abstract class Connection {
6075
executeQueryOptions: ExecuteQueryOptions
6176
): Record<string, string | undefined> {
6277
const { settings } = executeQueryOptions;
63-
const { database } = this.options;
64-
return { database, ...settings };
78+
79+
// convert all settings values to string
80+
const strSettings = Object.entries(settings ?? {}).reduce<
81+
Record<string, string>
82+
>((acc, [key, value]) => {
83+
if (value !== undefined) {
84+
acc[key] = value.toString();
85+
}
86+
return acc;
87+
}, {});
88+
89+
return { ...this.parameters, ...strSettings };
90+
}
91+
92+
private handleUpdateParametersHeader(headerValue: string) {
93+
const updateParameters = headerValue
94+
.split(",")
95+
.reduce((acc: Record<string, string>, param) => {
96+
const [key, value] = param.split("=");
97+
if (allowedUpdateParameters.includes(key)) {
98+
acc[key] = value.trim();
99+
}
100+
return acc;
101+
}, {});
102+
this.parameters = {
103+
...this.parameters,
104+
...updateParameters
105+
};
106+
}
107+
108+
private handleResetSessionHeader() {
109+
const remainingParameters: Record<string, string> = {};
110+
for (const key in this.parameters) {
111+
if (immutableParameters.includes(key)) {
112+
remainingParameters[key] = this.parameters[key];
113+
}
114+
}
115+
this.parameters = remainingParameters;
65116
}
66117

67-
private processHeaders(headers: Headers) {
118+
private async handleUpdateEndpointHeader(headerValue: string): Promise<void> {
119+
const url = new URL(
120+
headerValue.startsWith("http") ? headerValue : `https://${headerValue}`
121+
);
122+
const newParams = Object.fromEntries(url.searchParams.entries());
123+
124+
// Validate account_id if present
125+
const currentAccountId =
126+
this.accountInfo?.id ?? (await this.resolveAccountId());
127+
if (newParams.account_id && currentAccountId !== newParams.account_id) {
128+
throw new ConnectionError({
129+
message: `Failed to execute USE ENGINE command. Account parameter mismatch. Contact support.`
130+
});
131+
}
132+
133+
// Remove url parameters and update engineEndpoint
134+
this.engineEndpoint = url.toString().replace(url.search, "");
135+
this.parameters = {
136+
...this.parameters,
137+
...newParams
138+
};
139+
}
140+
141+
private async processHeaders(headers: Headers) {
68142
const updateHeaderValue = headers.get(updateParametersHeader);
69143
if (updateHeaderValue) {
70-
const updateParameters = updateHeaderValue
71-
.split(",")
72-
.reduce((acc: Record<string, string>, param) => {
73-
const [key, value] = param.split("=");
74-
if (allowedUpdateParameters.includes(key)) {
75-
acc[key] = value.trim();
76-
}
77-
return acc;
78-
}, {});
79-
80-
if (updateParameters.database) {
81-
this.options.database = updateParameters.database;
82-
delete updateParameters.database;
83-
}
84-
this.options.additionalParameters = {
85-
...this.options.additionalParameters,
86-
...updateParameters
87-
};
144+
this.handleUpdateParametersHeader(updateHeaderValue);
145+
}
146+
147+
if (headers.has(resetSessionHeader)) {
148+
this.handleResetSessionHeader();
149+
}
150+
151+
const updateEndpointValue = headers.get(updateEndpointHeader);
152+
if (updateEndpointValue) {
153+
await this.handleUpdateEndpointHeader(updateEndpointValue);
88154
}
89155
}
90156

@@ -94,11 +160,6 @@ export abstract class Connection {
94160
): Promise<Statement> {
95161
const { httpClient, queryFormatter } = this.context;
96162

97-
executeQueryOptions.settings = {
98-
...defaultQuerySettings,
99-
...(executeQueryOptions.settings ?? {})
100-
};
101-
102163
executeQueryOptions.response = {
103164
...defaultResponseSettings,
104165
...(executeQueryOptions.response ?? {})
@@ -124,7 +185,7 @@ export abstract class Connection {
124185

125186
try {
126187
const response = await request.ready();
127-
this.processHeaders(response.headers);
188+
await this.processHeaders(response.headers);
128189
const statement = new Statement(this.context, {
129190
query: formattedQuery,
130191
request,

src/connection/connection_v2.ts

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ import {
1212
} from "../common/api";
1313

1414
import { Connection as BaseConnection } from "./base";
15+
import * as path from "path";
1516

1617
export class ConnectionV2 extends BaseConnection {
17-
private accountId: string | undefined;
18-
1918
private get account(): string {
2019
if (!this.options.account) {
2120
throw new Error("Account name is required");
@@ -31,7 +30,8 @@ export class ConnectionV2 extends BaseConnection {
3130
const { engineUrl } = await httpClient
3231
.request<{ engineUrl: string }>("GET", url)
3332
.ready();
34-
return engineUrl;
33+
// cut off query parameters that go after ?
34+
return engineUrl.split("?")[0];
3535
} catch (e) {
3636
if (e instanceof ApiError && e.status == 404) {
3737
throw new AccountNotFoundError({ account_name: accountName });
@@ -125,57 +125,72 @@ export class ConnectionV2 extends BaseConnection {
125125
return res[0];
126126
}
127127

128-
async resolveAccountId() {
128+
async resolveAccountInfo() {
129129
const { httpClient, apiEndpoint } = this.context;
130130
const url = `${apiEndpoint}/${ACCOUNT_ID_BY_NAME(this.account)}`;
131-
const { id } = await httpClient
132-
.request<{ id: string; region: string }>("GET", url)
131+
const { id, infraVersion } = await httpClient
132+
.request<{ id: string; region: string; infraVersion: string }>("GET", url)
133133
.ready();
134-
return id;
134+
return { id, infraVersion: parseInt(infraVersion ?? "1") };
135+
}
136+
137+
async resolveAccountId() {
138+
const accInfo = await this.resolveAccountInfo();
139+
return accInfo.id;
135140
}
136141

137142
async resolveEngineEndpoint() {
138143
const { engineName, database } = this.options;
139144
// Connect to system engine first
140145
const systemUrl = await this.getSystemEngineEndpoint();
141-
this.engineEndpoint = `${systemUrl}/${QUERY_URL}`;
142-
this.accountId = await this.resolveAccountId();
143-
if (engineName && database) {
144-
const engineEndpoint = await this.getEngineByNameAndDb(
145-
engineName,
146-
database
147-
);
148-
this.engineEndpoint = engineEndpoint;
149-
// Account id is no longer needed
150-
this.accountId = undefined;
151-
return this.engineEndpoint;
152-
}
153-
if (engineName) {
154-
const database = await this.getEngineDatabase(engineName);
155-
if (!database) {
156-
throw new AccessError({
157-
message: `Engine ${engineName} is attached to a database that current user can not access.`
158-
});
146+
this.engineEndpoint = path.join(systemUrl, QUERY_URL);
147+
this.accountInfo = await this.resolveAccountInfo();
148+
149+
if (this.accountInfo.infraVersion >= 2) {
150+
if (database) {
151+
await this.execute(`USE DATABASE ${database}`);
159152
}
160-
const engineEndpoint = await this.getEngineByNameAndDb(
161-
engineName,
162-
database
163-
);
164-
this.options.database = database;
165-
this.engineEndpoint = engineEndpoint;
166-
// Account id is no longer needed
167-
this.accountId = undefined;
168-
return this.engineEndpoint;
153+
if (engineName) {
154+
await this.execute(`USE ENGINE ${engineName}`);
155+
}
156+
} else {
157+
if (engineName && database) {
158+
const engineEndpoint = await this.getEngineByNameAndDb(
159+
engineName,
160+
database
161+
);
162+
this.engineEndpoint = engineEndpoint;
163+
// Account id is no longer needed
164+
this.accountInfo = undefined;
165+
return this.engineEndpoint;
166+
}
167+
if (engineName) {
168+
const database = await this.getEngineDatabase(engineName);
169+
if (!database) {
170+
throw new AccessError({
171+
message: `Engine ${engineName} is attached to a database that current user can not access.`
172+
});
173+
}
174+
const engineEndpoint = await this.getEngineByNameAndDb(
175+
engineName,
176+
database
177+
);
178+
this.parameters["database"] = database;
179+
this.engineEndpoint = engineEndpoint;
180+
// Account id is no longer needed
181+
this.accountInfo = undefined;
182+
return this.engineEndpoint;
183+
}
184+
// If nothing specified connect to generic system engine
169185
}
170-
// If nothing specified connect to generic system engine
171186
return this.engineEndpoint;
172187
}
173188

174189
protected getBaseParameters(
175190
executeQueryOptions: ExecuteQueryOptions
176191
): Record<string, string | undefined> {
177192
return {
178-
account_id: this.accountId,
193+
account_id: this.accountInfo?.id,
179194
...super.getBaseParameters(executeQueryOptions)
180195
};
181196
}

src/http/node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const DEFAULT_ERROR = "Server error";
2626
const DEFAULT_USER_AGENT = systemInfoString();
2727

2828
const PROTOCOL_VERSION_HEADER = "Firebolt-Protocol-Version";
29-
const PROTOCOL_VERSION = "2.0";
29+
const PROTOCOL_VERSION = "2.1";
3030
const createSocket = HttpsAgent.prototype.createSocket;
3131

3232
const agentOptions = {

src/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export enum OutputFormat {
2929
JSON = "JSON"
3030
}
3131

32-
export type QuerySettings = Record<string, unknown> & {
32+
export type QuerySettings = Record<
33+
string,
34+
string | number | boolean | undefined
35+
> & {
3336
output_format?: OutputFormat;
3437
};
3538

test/integration/v2/account.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const connectionOptions = {
77
},
88
database: process.env.FIREBOLT_DATABASE as string,
99
engineName: process.env.FIREBOLT_ENGINE_NAME as string,
10-
account: process.env.FIREBOLT_ACCOUNT as string
10+
account: process.env.FIREBOLT_ACCOUNT_V1 as string
1111
};
1212

1313
jest.setTimeout(20000);

test/integration/v2/auth.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const auth = {
77

88
const connectionOptions = {
99
database: process.env.FIREBOLT_DATABASE as string,
10-
account: process.env.FIREBOLT_ACCOUNT as string
10+
account: process.env.FIREBOLT_ACCOUNT_V1 as string
1111
};
1212

1313
jest.setTimeout(20000);

test/integration/v2/boolean.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const connectionParams = {
55
client_id: process.env.FIREBOLT_CLIENT_ID as string,
66
client_secret: process.env.FIREBOLT_CLIENT_SECRET as string
77
},
8-
account: process.env.FIREBOLT_ACCOUNT as string,
8+
account: process.env.FIREBOLT_ACCOUNT_V1 as string,
99
database: process.env.FIREBOLT_DATABASE as string,
1010
engineName: process.env.FIREBOLT_ENGINE_NAME as string
1111
};

test/integration/v2/bytea.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const connectionParams = {
55
client_id: process.env.FIREBOLT_CLIENT_ID as string,
66
client_secret: process.env.FIREBOLT_CLIENT_SECRET as string
77
},
8-
account: process.env.FIREBOLT_ACCOUNT as string,
8+
account: process.env.FIREBOLT_ACCOUNT_V1 as string,
99
database: process.env.FIREBOLT_DATABASE as string,
1010
engineName: process.env.FIREBOLT_ENGINE_NAME as string
1111
};

test/integration/v2/database.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const connectionOptions = {
55
client_id: process.env.FIREBOLT_CLIENT_ID as string,
66
client_secret: process.env.FIREBOLT_CLIENT_SECRET as string
77
},
8-
account: process.env.FIREBOLT_ACCOUNT as string,
8+
account: process.env.FIREBOLT_ACCOUNT_V1 as string,
99
database: process.env.FIREBOLT_DATABASE as string
1010
};
1111

0 commit comments

Comments
 (0)