Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ console.log(rows)
* <a href="#execute-async-query">Execute Async Query</a>
* <a href="#check-async-query-status">Check Async Query Status</a>
* <a href="#cancel-async-query">Cancel Async Query</a>
* <a href="#transaction-management">Transaction management</a>
* <a href="#transaction-methods">Transaction methods</a>
* <a href="#basic-transaction-usage">Basic transaction usage</a>
* <a href="#transaction-error-handling">Error handling</a>
* <a href="#transaction-isolation">Transaction isolation</a>
* <a href="#engine-management">Engine management</a>
* <a href="#getbyname">getByName</a>
* <a href="#engine">Engine</a>
Expand Down Expand Up @@ -472,6 +477,140 @@ const token = statement.asyncQueryToken; // can only be fetched for async query
await connection.cancelAsyncQuery(token);
```

<a id="transaction-management"></a>
## Transaction management

Firebolt's Node.js SDK supports database transactions, allowing you to group multiple operations into atomic units of work. Transactions ensure data consistency and provide the ability to rollback changes if needed.

<a id="transaction-methods"></a>
### Transaction methods

The SDK provides three main methods for transaction management:

```typescript
await connection.begin(); // Start a new transaction
await connection.commit(); // Commit the current transaction
await connection.rollback(); // Rollback the current transaction
```

<a id="basic-transaction-usage"></a>
### Basic transaction usage

The following example demonstrates a basic transaction that inserts data and commits the changes:

```typescript
import { Firebolt } from 'firebolt-sdk'

const firebolt = Firebolt();
const connection = await firebolt.connect({
auth: {
client_id: process.env.FIREBOLT_CLIENT_ID,
client_secret: process.env.FIREBOLT_CLIENT_SECRET,
},
account: process.env.FIREBOLT_ACCOUNT,
database: process.env.FIREBOLT_DATABASE,
engineName: process.env.FIREBOLT_ENGINE_NAME
});

// Start a transaction
await connection.begin();

try {
// Perform multiple operations
await connection.execute(`
INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)
`);

await connection.execute(`
INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25)
`);

// Commit the transaction
await connection.commit();
console.log('Transaction committed successfully');
} catch (error) {
// Rollback on error
await connection.rollback();
console.error('Transaction rolled back due to error:', error);
}
```

#### Transaction with prepared statements

Transactions work seamlessly with prepared statements:

```typescript
await connection.begin();

try {
// Use prepared statements within transactions
await connection.execute(
'INSERT INTO users (id, name, age) VALUES (?, ?, ?)',
{ parameters: [4, 'Diana', 28] }
);

await connection.execute(
'UPDATE users SET age = ? WHERE id = ?',
{ parameters: [29, 4] }
);

await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
}
```

<a id="transaction-error-handling"></a>
### Error handling

#### Transaction state errors

The SDK will throw errors for invalid transaction operations:

```typescript
try {
// This will throw an error if no transaction is active
await connection.commit();
} catch (error) {
console.error('Cannot commit: no transaction in progress');
}

try {
await connection.begin();
// This will throw an error if a transaction is already active
await connection.begin();
} catch (error) {
console.error('Cannot begin: transaction already in progress');
}
```

<a id="transaction-isolation"></a>
### Transaction isolation

Transactions in Firebolt provide isolation between concurrent operations. Changes made within a transaction are not visible to other connections until the transaction is committed:

```typescript
// Connection 1 - Start transaction and insert data
const connection1 = await firebolt.connect(connectionOptions);
await connection1.begin();
await connection1.execute("INSERT INTO users (id, name) VALUES (5, 'Eve')");

// Connection 2 - Cannot see uncommitted data
const connection2 = await firebolt.connect(connectionOptions);
const statement = await connection2.execute('SELECT COUNT(*) FROM users WHERE id = 5');
const { data } = await statement.fetchResult();
console.log('Count from connection 2:', data[0][0]); // Should show 0

// Connection 1 - Commit transaction
await connection1.commit();

// Connection 2 - Now can see committed data
const statement2 = await connection2.execute('SELECT COUNT(*) FROM users WHERE id = 5');
const { data: data2 } = await statement2.fetchResult();
console.log('Count after commit:', data2[0][0]); // Should show 1
```

<a id="serverSidePreparedStatement"></a>
## Server-side prepared statement

Expand Down
19 changes: 15 additions & 4 deletions src/connection/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export const defaultResponseSettings = {
};

const updateParametersHeader = "Firebolt-Update-Parameters";
const allowedUpdateParameters = ["database"];
const updateEndpointHeader = "Firebolt-Update-Endpoint";
const resetSessionHeader = "Firebolt-Reset-Session";
const removeParametersHeader = "Firebolt-Remove-Parameters";
const immutableParameters = ["database", "account_id", "output_format"];
const testConnectionQuery = "SELECT 1";

Expand Down Expand Up @@ -108,9 +108,7 @@ export abstract class Connection {
.split(",")
.reduce((acc: Record<string, string>, param) => {
const [key, value] = param.split("=");
if (allowedUpdateParameters.includes(key)) {
acc[key] = value.trim();
}
acc[key] = value.trim();
return acc;
}, {});
this.parameters = {
Expand All @@ -119,6 +117,14 @@ export abstract class Connection {
};
}

private handleRemoveParametersHeader(headerValue: string) {
const removeParameters = headerValue.split(",");

removeParameters.forEach(key => {
delete this.parameters[key.trim()];
});
}

private handleResetSessionHeader() {
const remainingParameters: Record<string, string> = {};
for (const key in this.parameters) {
Expand Down Expand Up @@ -161,6 +167,11 @@ export abstract class Connection {
if (updateEndpointValue) {
await this.handleUpdateEndpointHeader(updateEndpointValue);
}

const removeParametersValue = headers.get(removeParametersHeader);
if (removeParametersValue) {
this.handleRemoveParametersHeader(removeParametersValue);
}
}

abstract executeAsync(
Expand Down
18 changes: 18 additions & 0 deletions src/connection/connection_v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,22 @@ export class ConnectionV1 extends BaseConnection {
"Stream execution is not supported in this Firebolt version."
);
}

async begin(): Promise<void> {
throw new Error(
"Transaction management is not supported in this Firebolt version."
);
}

async commit(): Promise<void> {
throw new Error(
"Transaction management is not supported in this Firebolt version."
);
}

async rollback(): Promise<void> {
throw new Error(
"Transaction management is not supported in this Firebolt version."
);
}
}
12 changes: 12 additions & 0 deletions src/connection/connection_v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,16 @@ export class ConnectionV2 extends BaseConnection {
const settings = { internal: [{ auto_start_stop_control: "ignore" }] };
await this.execute("select 1", { settings });
}

async begin(): Promise<void> {
await this.execute("BEGIN TRANSACTION");
}

async commit(): Promise<void> {
await this.execute("COMMIT");
}

async rollback(): Promise<void> {
await this.execute("ROLLBACK");
}
}
2 changes: 1 addition & 1 deletion src/http/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const DEFAULT_ERROR = "Server error";
const DEFAULT_USER_AGENT = systemInfoString();

const PROTOCOL_VERSION_HEADER = "Firebolt-Protocol-Version";
const PROTOCOL_VERSION = "2.3";
const PROTOCOL_VERSION = "2.4";
const createSocket = HttpsAgent.prototype.createSocket;

const agentOptions = {
Expand Down
38 changes: 38 additions & 0 deletions test/integration/v1/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,42 @@ describe("integration test", () => {

data.pipe(process.stdout);
});

describe("Transaction methods", () => {
it("throws error for begin transaction", async () => {
const firebolt = Firebolt({
apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string
});

const connection = await firebolt.connect(connectionParams);

await expect(connection.begin()).rejects.toThrow(
"Transaction management is not supported in this Firebolt version."
);
});

it("throws error for commit", async () => {
const firebolt = Firebolt({
apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string
});

const connection = await firebolt.connect(connectionParams);

await expect(connection.commit()).rejects.toThrow(
"Transaction management is not supported in this Firebolt version."
);
});

it("throws error for rollback", async () => {
const firebolt = Firebolt({
apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string
});

const connection = await firebolt.connect(connectionParams);

await expect(connection.rollback()).rejects.toThrow(
"Transaction management is not supported in this Firebolt version."
);
});
});
});
Loading