Skip to content

Commit 82a3b01

Browse files
feat: FIR-44270 Support server side prepared statements (#140)
1 parent 4e9921d commit 82a3b01

File tree

6 files changed

+1091
-40
lines changed

6 files changed

+1091
-40
lines changed

README.md

Lines changed: 124 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,11 @@ console.log(rows)
125125
* <a href="#usage">Usage</a>
126126
* <a href="#create-connection">Create connection</a>
127127
* <a href="#connectionoptions">ConnectionOptions</a>
128-
* <a href="#accesstoken">AccessToken</a>
129-
* <a href="#clientcredentials">Client credentials</a>
130-
* <a href="#enginename">engineName</a>
128+
* <a href="#accesstoken">AccessToken</a>
129+
* <a href="#clientcredentials">Client credentials</a>
130+
* <a href="#enginename">engineName</a>
131+
* <a href="#token-caching">Token caching</a>
132+
* <a href="#serverSidePreparedStatementConnectionOption">Server-side prepared statement</a>
131133
* <a href="#test-connection">Test connection</a>
132134
* <a href="#engine-url">Engine URL</a>
133135
* <a href="#execute-query">Execute query</a>
@@ -146,14 +148,21 @@ console.log(rows)
146148
* <a href="#engine-management">Engine management</a>
147149
* <a href="#getbyname">getByName</a>
148150
* <a href="#engine">Engine</a>
149-
* <a href="#start">start</a>
150-
* <a href="#stop">stop</a>
151+
* <a href="#start">Start</a>
152+
* <a href="#stop">Stop</a>
153+
* <a href="#create-engine">Engine create</a>
154+
* <a href="#attach-to-database">Attach to database</a>
155+
* <a href="#delete-engine">Engine delete</a>
151156
* <a href="#database-management">Database management</a>
152157
* <a href="#database-getbyname">getByName</a>
153158
* <a href="#database">Database</a>
159+
* <a href="#create-database">Database create</a>
160+
* <a href="#get-attached-engines">Get attached engines</a>
161+
* <a href="#delete-database">Database delete</a>
154162
* <a href="#recipes">Recipes</a>
155163
* <a href="#streaming-results">Streaming results</a>
156164
* <a href="#custom-stream-transformers">Custom stream transformers</a>
165+
* <a href="#in-memory-stream">In-memory stream</a>
157166
* <a href="#special-considerations">Special Considerations</a>
158167

159168
<a id="About"></a>
@@ -187,20 +196,18 @@ type ClientCredentialsAuth = {
187196
client_secret: string;
188197
};
189198

199+
type PreparedStatementParamStyle = "native" | "fb_numeric";
200+
190201
type ConnectionOptions = {
191202
auth: AccessTokenAuth | ServiceAccountAuth;
192203
database: string;
193204
engineName?: string;
194205
engineEndpoint?: string;
195206
account?: string;
207+
preparedStatementParamStyle?: PreparedStatementParamStyle;
196208
};
197209
```
198210

199-
200-
<a id="enginename"></a>
201-
#### engineName
202-
You can omit `engineName` and execute AQL queries on such connection.
203-
204211
<a id="accesstoken"></a>
205212
#### AccessToken
206213
Instead of passing client id/secret directly,
@@ -234,6 +241,10 @@ const connection = await firebolt.connect({
234241
});
235242
```
236243

244+
<a id="enginename"></a>
245+
#### engineName
246+
You can omit `engineName` and execute AQL queries on such connection.
247+
237248
<a id="token-caching"></a>
238249
#### Token caching
239250
Driver implements a caching mechanism for access tokens. If you are using the same client id or secret for multiple connections, the driver will cache the access token and reuse it for subsequent connections.
@@ -251,6 +262,23 @@ const connection = await firebolt.connect({
251262
});
252263
```
253264

265+
<a id="serverSidePreparedStatementConnectionOption"></a>
266+
#### Server-side prepared statement
267+
Driver has the option to use server-side prepared statements, so all parameters are set on the server side, preventing SQL injection attacks.
268+
This behavior can be enabled by setting `preparedStatementParamStyle` to `fb_numeric` in the connection options, otherwise, prepared statements will retain default behavior(same behavior if value is set to `native`) and queries will be formatted client side.
269+
```typescript
270+
const connection = await firebolt.connect({
271+
auth: {
272+
client_id: 'b1c4918c-e07e-4ab2-868b-9ae84f208d26',
273+
client_secret: 'secret',
274+
},
275+
engineName: 'engine_name',
276+
account: 'account_name',
277+
database: 'database',
278+
preparedStatementParamStyle: 'fb_numeric'
279+
});
280+
```
281+
254282

255283
<a id="test-connection"></a>
256284
### Test connection
@@ -291,7 +319,7 @@ const statement = await connection.execute(query, {
291319

292320
```typescript
293321
export type ExecuteQueryOptions = {
294-
parameters:? unknown[];
322+
parameters?: unknown[];
295323
settings?: QuerySettings;
296324
response?: ResponseSettings;
297325
};
@@ -444,8 +472,82 @@ const token = statement.asyncQueryToken; // can only be fetched for async query
444472
await connection.cancelAsyncQuery(token);
445473
```
446474

475+
<a id="serverSidePreparedStatement"></a>
476+
## Server-side prepared statement
477+
478+
Firebolt supports server-side prepared statement execution. This feature allows for safer execution of parameterized queries by escaping parameters on the server side. This is useful for preventing SQL injection attacks.
479+
480+
<a id="difference-server-side-client-side-prepared-statement"></a>
481+
### Difference between client-side and server-side prepared statement
482+
483+
The main difference between client-side and server-side prepared statement is the way parameters appear in queries. In client-side prepared statement, parameters are inserted in place of `?` symbols in the case of normal parameters, or `:name` in the case of named parameters.
484+
In server-side prepared statement, parameters are represented by `$number` tokens.
485+
486+
```typescript
487+
//client-side prepared statement with normal parameters
488+
const statement = await connection.execute("select ?, ?", {
489+
parameters: ["foo", 1]
490+
});
491+
```
492+
```typescript
493+
//client-side prepared statement with named parameters
494+
const statement = await connection.execute("select :foo, :bar", {
495+
namedParameters: { foo: "foo", bar: 123 }
496+
});
497+
```
498+
```typescript
499+
//server-side prepared statement via parameters field of ExecuteQueryOptions
500+
const statement = await connection.execute("select $1, $2", {
501+
parameters: ["foo", 1]
502+
});
503+
```
504+
```typescript
505+
//server-side prepared statement via namedParameters field of ExecuteQueryOptions
506+
const statement = await connection.execute("select $1, $2", {
507+
namedParameters: { $1: "foo", $2: 123 }
508+
});
509+
```
510+
511+
<a id="server-side-prepared-statement-parameters"></a>
512+
### Usage with parameters field
513+
514+
When using the `parameters` field, the driver will automatically set the number value to the corresponding `$number` token in the query.
515+
516+
```typescript
517+
// Even though the query contains $1 twice, we only need to set it once
518+
const statement = await connection.execute("select $1, $1", {
519+
parameters: ["foo"]
520+
});
521+
522+
// The order is important, so the first parameter will be set to $1 and the second to $2
523+
const statement1 = await connection.execute("select $2, $1", {
524+
parameters: ["foo", 1]
525+
});
526+
527+
const statement2 = await connection.execute("select $1, $2", {
528+
parameters: ["foo", 1]
529+
});
530+
// statement1 and statement2 will NOT produce the same query
531+
```
532+
533+
<a id="server-side-prepared-statement-named-parameters"></a>
534+
### Usage with namedParameters field
535+
536+
When using the `namedParameters` field, the driver will use the value provided as the name of the parameter when sending the query to the server.
537+
Considering this, we can more easily recognize the parameters in the query.
538+
539+
```typescript
540+
const statement = await connection.execute("select $1, $2", {
541+
namedParameters: { $1: "foo", $2: 123 }
542+
});
543+
// The order is not important, so we can set the parameters in any order
544+
const statement1 = await connection.execute("select $2, $1", {
545+
namedParameters: { $2: "foo", $1: 123 }
546+
});
547+
```
548+
447549
<a id="engine-management"></a>
448-
### Engine management
550+
## Engine management
449551

450552
Engines can be managed by using the `resourceManager` object.
451553

@@ -457,7 +559,7 @@ const enginesService = firebolt.resourceManager.engine
457559
```
458560

459561
<a id="getbyname"></a>
460-
#### getByName
562+
### getByName
461563

462564
Returns engine using engine name.
463565

@@ -469,7 +571,7 @@ const engine = await firebolt.resourceManager.engine.getByName("engine_name")
469571
```
470572

471573
<a id="engine"></a>
472-
#### Engine
574+
### Engine
473575

474576
| Property | Type | Notes |
475577
|--------------------------|-------------------------------------------|-------|
@@ -478,7 +580,7 @@ const engine = await firebolt.resourceManager.engine.getByName("engine_name")
478580
| `current_status_summary` | `string` | |
479581

480582
<a id="start"></a>
481-
##### Start
583+
#### Start
482584

483585
Starts an engine.
484586

@@ -491,7 +593,7 @@ await engine.start()
491593
```
492594

493595
<a id="stop"></a>
494-
##### Stop
596+
#### Stop
495597

496598
Stops an engine.
497599

@@ -504,7 +606,7 @@ await engine.stop()
504606
```
505607

506608
<a id="create-engine"></a>
507-
##### Engine create
609+
#### Engine create
508610

509611
Creates an engine.
510612

@@ -516,7 +618,7 @@ const engine = await firebolt.resourceManager.engine.create("engine_name");
516618
```
517619

518620
<a id="attach-to-database"></a>
519-
##### Attach to database
621+
#### Attach to database
520622

521623
Attaches an engine to a database.
522624

@@ -528,7 +630,7 @@ const engine = await firebolt.resourceManager.engine.attachToDatabase("engine_na
528630
```
529631

530632
<a id="delete-engine"></a>
531-
##### Engine delete
633+
#### Engine delete
532634

533635
Deletes an engine.
534636

@@ -574,7 +676,7 @@ const database = await firebolt.resourceManager.database.getByName("database_nam
574676

575677

576678
<a id="create-database"></a>
577-
##### Database create
679+
#### Database create
578680

579681
Creates a database.
580682

@@ -586,7 +688,7 @@ const database = await firebolt.resourceManager.database.create("database_name")
586688
```
587689

588690
<a id="get-attached-engines"></a>
589-
##### Get attached engines
691+
#### Get attached engines
590692

591693
Get engines attached to a database.
592694

@@ -599,7 +701,7 @@ const engines = database.getAttachedEngines();
599701
```
600702

601703
<a id="delete-database"></a>
602-
##### Database delete
704+
#### Database delete
603705

604706
Deletes a database.
605707

src/connection/base.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,10 @@ export abstract class Connection {
179179

180180
abstract cancelAsyncQuery(token: string): Promise<void>;
181181

182-
protected async prepareAndExecuteQuery(
182+
protected prepareQuery(
183183
query: string,
184184
executeQueryOptions: ExecuteQueryOptions
185-
): Promise<{ formattedQuery: string; response: Response }> {
186-
const { httpClient } = this.context;
187-
185+
): { formattedQuery: string; setKey: string } {
188186
executeQueryOptions.response = {
189187
...defaultResponseSettings,
190188
...(executeQueryOptions.response ?? {})
@@ -207,6 +205,17 @@ export abstract class Connection {
207205
);
208206
}
209207

208+
return { formattedQuery, setKey };
209+
}
210+
211+
protected async executeQuery(
212+
formattedQuery: string,
213+
executeQueryOptions: ExecuteQueryOptions,
214+
setKey = "",
215+
returnResponse = false
216+
) {
217+
const { httpClient } = this.context;
218+
210219
const body = formattedQuery;
211220
const url = this.getRequestUrl(executeQueryOptions);
212221

@@ -221,7 +230,14 @@ export abstract class Connection {
221230
try {
222231
const response = await request.ready();
223232
await this.processHeaders(response.headers);
224-
return { formattedQuery, response };
233+
if (returnResponse) {
234+
return { response, text: "" };
235+
}
236+
237+
const text = await response.text();
238+
await this.throwErrorIfErrorBody(text, response);
239+
240+
return { response, text };
225241
} catch (error) {
226242
// In case it was a set query, remove set parameter if query fails
227243
if (setKey.length > 0) {
@@ -233,17 +249,42 @@ export abstract class Connection {
233249
}
234250
}
235251

252+
protected async prepareAndExecuteQuery(
253+
query: string,
254+
executeQueryOptions: ExecuteQueryOptions,
255+
returnResponse = false
256+
): Promise<{
257+
formattedQuery: string;
258+
response: Response;
259+
text: string;
260+
}> {
261+
const { formattedQuery, setKey } = this.prepareQuery(
262+
query,
263+
executeQueryOptions
264+
);
265+
const { response, text } = await this.executeQuery(
266+
formattedQuery,
267+
executeQueryOptions,
268+
setKey,
269+
returnResponse
270+
);
271+
272+
return {
273+
formattedQuery,
274+
response,
275+
text
276+
};
277+
}
278+
236279
async execute(
237280
query: string,
238281
executeQueryOptions: ExecuteQueryOptions = {}
239282
): Promise<Statement> {
240-
const { formattedQuery, response } = await this.prepareAndExecuteQuery(
283+
const { formattedQuery, text } = await this.prepareAndExecuteQuery(
241284
query,
242285
executeQueryOptions
243286
);
244287

245-
const text = await response.text();
246-
await this.throwErrorIfErrorBody(text, response);
247288
return new Statement(this.context, {
248289
query: formattedQuery,
249290
text,

0 commit comments

Comments
 (0)