UNPKG

@libsql/client

Version:

libSQL driver for TypeScript and JavaScript

331 lines (330 loc) 13.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.mapHranaError = exports.resultSetFromHrana = exports.stmtToHrana = exports.executeHranaBatch = exports.HranaTransaction = void 0; const hrana = __importStar(require("@libsql/hrana-client")); const api_1 = require("@libsql/core/api"); const util_1 = require("@libsql/core/util"); class HranaTransaction { #mode; #version; // Promise that is resolved when the BEGIN statement completes, or `undefined` if we haven't executed the // BEGIN statement yet. #started; /** @private */ constructor(mode, version) { this.#mode = mode; this.#version = version; this.#started = undefined; } execute(stmt) { return this.batch([stmt]).then((results) => results[0]); } async batch(stmts) { const stream = this._getStream(); if (stream.closed) { throw new api_1.LibsqlError("Cannot execute statements because the transaction is closed", "TRANSACTION_CLOSED"); } try { const hranaStmts = stmts.map(stmtToHrana); let rowsPromises; if (this.#started === undefined) { // The transaction hasn't started yet, so we need to send the BEGIN statement in a batch with // `hranaStmts`. this._getSqlCache().apply(hranaStmts); const batch = stream.batch(this.#version >= 3); const beginStep = batch.step(); const beginPromise = beginStep.run((0, util_1.transactionModeToBegin)(this.#mode)); // Execute the `hranaStmts` only if the BEGIN succeeded, to make sure that we don't execute it // outside of a transaction. let lastStep = beginStep; rowsPromises = hranaStmts.map((hranaStmt) => { const stmtStep = batch .step() .condition(hrana.BatchCond.ok(lastStep)); if (this.#version >= 3) { // If the Hrana version supports it, make sure that we are still in a transaction stmtStep.condition(hrana.BatchCond.not(hrana.BatchCond.isAutocommit(batch))); } const rowsPromise = stmtStep.query(hranaStmt); rowsPromise.catch(() => undefined); // silence Node warning lastStep = stmtStep; return rowsPromise; }); // `this.#started` is resolved successfully only if the batch and the BEGIN statement inside // of the batch are both successful. this.#started = batch .execute() .then(() => beginPromise) .then(() => undefined); try { await this.#started; } catch (e) { // If the BEGIN failed, the transaction is unusable and we must close it. However, if the // BEGIN suceeds and `hranaStmts` fail, the transaction is _not_ closed. this.close(); throw e; } } else { if (this.#version < 3) { // The transaction has started, so we must wait until the BEGIN statement completed to make // sure that we don't execute `hranaStmts` outside of a transaction. await this.#started; } else { // The transaction has started, but we will use `hrana.BatchCond.isAutocommit()` to make // sure that we don't execute `hranaStmts` outside of a transaction, so we don't have to // wait for `this.#started` } this._getSqlCache().apply(hranaStmts); const batch = stream.batch(this.#version >= 3); let lastStep = undefined; rowsPromises = hranaStmts.map((hranaStmt) => { const stmtStep = batch.step(); if (lastStep !== undefined) { stmtStep.condition(hrana.BatchCond.ok(lastStep)); } if (this.#version >= 3) { stmtStep.condition(hrana.BatchCond.not(hrana.BatchCond.isAutocommit(batch))); } const rowsPromise = stmtStep.query(hranaStmt); rowsPromise.catch(() => undefined); // silence Node warning lastStep = stmtStep; return rowsPromise; }); await batch.execute(); } const resultSets = []; for (const rowsPromise of rowsPromises) { const rows = await rowsPromise; if (rows === undefined) { throw new api_1.LibsqlError("Statement in a transaction was not executed, " + "probably because the transaction has been rolled back", "TRANSACTION_CLOSED"); } resultSets.push(resultSetFromHrana(rows)); } return resultSets; } catch (e) { throw mapHranaError(e); } } async executeMultiple(sql) { const stream = this._getStream(); if (stream.closed) { throw new api_1.LibsqlError("Cannot execute statements because the transaction is closed", "TRANSACTION_CLOSED"); } try { if (this.#started === undefined) { // If the transaction hasn't started yet, start it now this.#started = stream .run((0, util_1.transactionModeToBegin)(this.#mode)) .then(() => undefined); try { await this.#started; } catch (e) { this.close(); throw e; } } else { // Wait until the transaction has started await this.#started; } await stream.sequence(sql); } catch (e) { throw mapHranaError(e); } } async rollback() { try { const stream = this._getStream(); if (stream.closed) { return; } if (this.#started !== undefined) { // We don't have to wait for the BEGIN statement to complete. If the BEGIN fails, we will // execute a ROLLBACK outside of an active transaction, which should be harmless. } else { // We did nothing in the transaction, so there is nothing to rollback. return; } // Pipeline the ROLLBACK statement and the stream close. const promise = stream.run("ROLLBACK").catch((e) => { throw mapHranaError(e); }); stream.closeGracefully(); await promise; } catch (e) { throw mapHranaError(e); } finally { // `this.close()` may close the `hrana.Client`, which aborts all pending stream requests, so we // must call it _after_ we receive the ROLLBACK response. // Also note that the current stream should already be closed, but we need to call `this.close()` // anyway, because it may need to do more cleanup. this.close(); } } async commit() { // (this method is analogous to `rollback()`) try { const stream = this._getStream(); if (stream.closed) { throw new api_1.LibsqlError("Cannot commit the transaction because it is already closed", "TRANSACTION_CLOSED"); } if (this.#started !== undefined) { // Make sure to execute the COMMIT only if the BEGIN was successful. await this.#started; } else { return; } const promise = stream.run("COMMIT").catch((e) => { throw mapHranaError(e); }); stream.closeGracefully(); await promise; } catch (e) { throw mapHranaError(e); } finally { this.close(); } } } exports.HranaTransaction = HranaTransaction; async function executeHranaBatch(mode, version, batch, hranaStmts, disableForeignKeys = false) { if (disableForeignKeys) { batch.step().run("PRAGMA foreign_keys=off"); } const beginStep = batch.step(); const beginPromise = beginStep.run((0, util_1.transactionModeToBegin)(mode)); let lastStep = beginStep; const stmtPromises = hranaStmts.map((hranaStmt) => { const stmtStep = batch.step().condition(hrana.BatchCond.ok(lastStep)); if (version >= 3) { stmtStep.condition(hrana.BatchCond.not(hrana.BatchCond.isAutocommit(batch))); } const stmtPromise = stmtStep.query(hranaStmt); lastStep = stmtStep; return stmtPromise; }); const commitStep = batch.step().condition(hrana.BatchCond.ok(lastStep)); if (version >= 3) { commitStep.condition(hrana.BatchCond.not(hrana.BatchCond.isAutocommit(batch))); } const commitPromise = commitStep.run("COMMIT"); const rollbackStep = batch .step() .condition(hrana.BatchCond.not(hrana.BatchCond.ok(commitStep))); rollbackStep.run("ROLLBACK").catch((_) => undefined); if (disableForeignKeys) { batch.step().run("PRAGMA foreign_keys=on"); } await batch.execute(); const resultSets = []; await beginPromise; for (const stmtPromise of stmtPromises) { const hranaRows = await stmtPromise; if (hranaRows === undefined) { throw new api_1.LibsqlError("Statement in a batch was not executed, probably because the transaction has been rolled back", "TRANSACTION_CLOSED"); } resultSets.push(resultSetFromHrana(hranaRows)); } await commitPromise; return resultSets; } exports.executeHranaBatch = executeHranaBatch; function stmtToHrana(stmt) { if (typeof stmt === "string") { return new hrana.Stmt(stmt); } const hranaStmt = new hrana.Stmt(stmt.sql); if (Array.isArray(stmt.args)) { hranaStmt.bindIndexes(stmt.args); } else { for (const [key, value] of Object.entries(stmt.args)) { hranaStmt.bindName(key, value); } } return hranaStmt; } exports.stmtToHrana = stmtToHrana; function resultSetFromHrana(hranaRows) { const columns = hranaRows.columnNames.map((c) => c ?? ""); const columnTypes = hranaRows.columnDecltypes.map((c) => c ?? ""); const rows = hranaRows.rows; const rowsAffected = hranaRows.affectedRowCount; const lastInsertRowid = hranaRows.lastInsertRowid !== undefined ? hranaRows.lastInsertRowid : undefined; return new util_1.ResultSetImpl(columns, columnTypes, rows, rowsAffected, lastInsertRowid); } exports.resultSetFromHrana = resultSetFromHrana; function mapHranaError(e) { if (e instanceof hrana.ClientError) { const code = mapHranaErrorCode(e); return new api_1.LibsqlError(e.message, code, undefined, e); } return e; } exports.mapHranaError = mapHranaError; function mapHranaErrorCode(e) { if (e instanceof hrana.ResponseError && e.code !== undefined) { return e.code; } else if (e instanceof hrana.ProtoError) { return "HRANA_PROTO_ERROR"; } else if (e instanceof hrana.ClosedError) { return e.cause instanceof hrana.ClientError ? mapHranaErrorCode(e.cause) : "HRANA_CLOSED_ERROR"; } else if (e instanceof hrana.WebSocketError) { return "HRANA_WEBSOCKET_ERROR"; } else if (e instanceof hrana.HttpServerError) { return "SERVER_ERROR"; } else if (e instanceof hrana.ProtocolVersionError) { return "PROTOCOL_VERSION_ERROR"; } else if (e instanceof hrana.InternalError) { return "INTERNAL_ERROR"; } else { return "UNKNOWN"; } }