Added the ability to execute sql outside of transactions with the database.sql method

This commit is contained in:
Ben Ashton 2021-03-16 11:26:59 -07:00
parent da2a186c95
commit 7387e20b09
8 changed files with 278 additions and 236 deletions

View File

@ -61,7 +61,7 @@ const db = await PawSQLite.open("test", {
### Querying the database:
You can query the database by using the `sql` method on a transaction object. For convenience, database objects also contain an `sql` method which is shorthand for `db.autoTransaction((tx) => tx.sql(sql, ...args));`. The `sql` method also allows you to bind parameters to the satatement. Bound parameters are escaped by whichever native SQLite implementation your chosen adapter uses.
You can query the database by using the `sql` method on a database or transaction object. The `sql` method also allows you to bind parameters to the satatement. Bound parameters are escaped by whichever native SQLite implementation your chosen adapter uses.
```javascript
await db.sql("SELECT * FROM contacts WHERE name=?", "Paul");

View File

@ -65,7 +65,11 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "Database": () => (/* binding */ Database)
/* harmony export */ });
/* harmony import */ var _transaction_manager_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./transaction_manager.mjs */ "./src/transaction_manager.mjs");
/* harmony import */ var _task_manager_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./task_manager.mjs */ "./src/task_manager.mjs");
/* harmony import */ var _transaction_mjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./transaction.mjs */ "./src/transaction.mjs");
/* harmony import */ var _query_mjs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./query.mjs */ "./src/query.mjs");
@ -76,7 +80,7 @@ class Database {
this.version = null;
this.path = null;
this.transactionManager = new _transaction_manager_mjs__WEBPACK_IMPORTED_MODULE_0__.TransactionManager(dbName, this.adapter);
this.taskManager = new _task_manager_mjs__WEBPACK_IMPORTED_MODULE_0__.TaskManager();
}
async open() {
@ -100,10 +104,13 @@ class Database {
}
transaction() {
return this.transactionManager.transaction();
return new _transaction_mjs__WEBPACK_IMPORTED_MODULE_1__.Transaction(
this.dbName,
this.adapter,
this.taskManager.enqueue.bind(this.taskManager)
);
}
async autoTransaction(cb, inheritTx) {
let tx = inheritTx || this.transaction();
let result;
@ -125,9 +132,23 @@ class Database {
}
// Helper method to start a transaction and execute a single SQL statement
sql(sql, ...args) {
return this.autoTransaction((tx) => tx.sql(sql, ...args));
// Execute a single SQL statement
async sql(sql, ...args) {
const completeCb = await this.taskManager.enqueue();
let result;
let error;
try {
result = await this.adapter.sql(this.dbName, ...(0,_query_mjs__WEBPACK_IMPORTED_MODULE_2__.query)(sql, ...args));
} catch (e) {
error = e;
}
completeCb();
if (error) {
throw error;
}
return new Result(result);
}
}
@ -258,6 +279,68 @@ class PawSQLiteError extends Error {
}
}
/***/ }),
/***/ "./src/query.mjs":
/*!***********************!*\
!*** ./src/query.mjs ***!
\***********************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "query": () => (/* binding */ query),
/* harmony export */ "buildQuery": () => (/* binding */ buildQuery),
/* harmony export */ "validateQuery": () => (/* binding */ validateQuery)
/* harmony export */ });
function query(sql, ...args) {
validateQuery(sql, ...args);
return buildQuery(sql, ...args);
}
// Allow for slightly more complex parameter substitution.
// Instances of "???" will be replaced by the same number of comma-separated
// question marks as items in the corresponding nested parateter array
// eg. buildQuery("SELECT (???) FROM ?", [["col1", "col2"], "table1"])
// would output: ["SELECT (?, ?) FROM ?", ["col1", "col2", "table1"]]
function buildQuery(sql, ...args) {
const parts = sql.split("???");
const subParamLengths = args
.filter(Array.isArray)
.map((a) => a.length);
if (parts.length !== subParamLengths.length + 1) {
throw new PawSQLiteError("Unable to build query: sub-" +
"paramters do not match sub-paramters in query");
}
const newQuery = parts.reduce((p1, p2, i) => {
const length = subParamLengths[i - 1];
return p1 + new Array(length).fill("?").join(", ") + p2;
});
const flatParams = args.reduce((acc, v) => {
if (Array.isArray(v)) {
Array.prototype.push.apply(acc, v);
} else {
acc.push(v);
}
return acc;
}, []);
return [newQuery, ...flatParams];
}
function validateQuery(sql, ...args) {
const reg = /^\s*(BEGIN|END|COMMIT|ROLLBACK)(?:[^A-Z]|$)/i;
const match = reg.exec(sql);
if (match) {
const statement = match[1].toUpperCase();
throw new PawSQLiteError("Manually managing transactions is " +
"forbidden. Found: \"" + statement + "\" statement.");
}
}
/***/ }),
/***/ "./src/result.mjs":
@ -293,6 +376,65 @@ class Result extends Array {
/***/ }),
/***/ "./src/task_manager.mjs":
/*!******************************!*\
!*** ./src/task_manager.mjs ***!
\******************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "TaskManager": () => (/* binding */ TaskManager)
/* harmony export */ });
class TaskManager {
constructor() {
this._queue = [];
this._inTask = false;
}
enqueue() {
let completeSignal;
let taskComplete = new Promise((r, _) => {
completeSignal = r;
});
let readySignal;
let dbReady = new Promise((r, _) => {
readySignal = () => {
r(completeSignal);
};
});
this._queue.push({readySignal, taskComplete});
this._processQueue();
return dbReady;
}
async _processQueue() {
// We're already processing the queue
if (this._inTask) {
return;
}
while (true) {
let item = this._queue.shift();
if (!item) {
return;
}
this._inTask = true;
item.readySignal();
await item.taskComplete;
this._inTask = false;
}
}
}
/***/ }),
/***/ "./src/transaction.mjs":
/*!*****************************!*\
!*** ./src/transaction.mjs ***!
@ -305,6 +447,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ });
/* harmony import */ var _result_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./result.mjs */ "./src/result.mjs");
/* harmony import */ var _pawsqlite_error_mjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./pawsqlite_error.mjs */ "./src/pawsqlite_error.mjs");
/* harmony import */ var _query_mjs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./query.mjs */ "./src/query.mjs");
@ -332,46 +476,16 @@ class Transaction {
await this._waitUntilReady();
}
let result;
try {
return await this._executeSQL(sql, ...args);
result = await this.adapter.sql(this.dbName, ...(0,_query_mjs__WEBPACK_IMPORTED_MODULE_2__.query)(sql, ...args));
} catch (e) {
if (this._rollbackOnError) {
await this.rollback();
}
throw e;
}
}
// Allow for slightly more complex parameter substitution.
// Instances of "???" will be replaced by the same number of comma-separated
// question marks as items in the corresponding nested parateter array
// eg. buildQuery("SELECT (???) FROM ?", [["col1", "col2"], "table1"])
// would output: ["SELECT (?, ?) FROM ?", ["col1", "col2", "table1"]]
buildQuery(sql, ...args) {
const parts = sql.split("???");
const subParamLengths = args
.filter(Array.isArray)
.map((a) => a.length);
if (parts.length !== subParamLengths.length + 1) {
throw new _pawsqlite_error_mjs__WEBPACK_IMPORTED_MODULE_1__.PawSQLiteError("Unable to build query: sub-" +
"paramters do not match sub-paramters in query");
}
const newQuery = parts.reduce((p1, p2, i) => {
const length = subParamLengths[i - 1];
return p1 + new Array(length).fill("?").join(", ") + p2;
});
const flatParams = args.reduce((acc, v) => {
if (Array.isArray(v)) {
Array.prototype.push.apply(acc, v);
} else {
acc.push(v);
}
return acc;
}, []);
return [newQuery, ...flatParams];
return new _result_mjs__WEBPACK_IMPORTED_MODULE_0__.Result(result);
}
commit() {
@ -394,21 +508,6 @@ class Transaction {
await this._readyWait;
}
async _executeSQL(sql, ...args) {
const reg = /^\s*(BEGIN|END|COMMIT|ROLLBACK)(?:[^A-Z]|$)/i;
const match = reg.exec(sql);
if (match) {
const statement = match[1].toUpperCase();
throw new _pawsqlite_error_mjs__WEBPACK_IMPORTED_MODULE_1__.PawSQLiteError("Manually managing transactions is " +
"forbidden. Found: \"" + statement + "\" statement.");
}
const result = await this.adapter.sql(this.dbName,
...this.buildQuery(sql, ...args));
return new _result_mjs__WEBPACK_IMPORTED_MODULE_0__.Result(result);
}
async _begin() {
const result = await this.adapter.sql(this.dbName, "BEGIN");
}
@ -444,75 +543,6 @@ class Transaction {
}
}
/***/ }),
/***/ "./src/transaction_manager.mjs":
/*!*************************************!*\
!*** ./src/transaction_manager.mjs ***!
\*************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "TransactionManager": () => (/* binding */ TransactionManager)
/* harmony export */ });
/* harmony import */ var _transaction_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./transaction.mjs */ "./src/transaction.mjs");
class TransactionManager {
constructor(dbName, adapter) {
this.dbName = dbName;
this.adapter = adapter;
this._queue = [];
this._inTransaction = false;
}
transaction() {
return new _transaction_mjs__WEBPACK_IMPORTED_MODULE_0__.Transaction(this.dbName, this.adapter,
this.enqueue.bind(this));
}
enqueue() {
let completeSignal;
let transactionComplete = new Promise((r, _) => {
completeSignal = r;
});
let readySignal;
let dbReady = new Promise((r, _) => {
readySignal = () => {
r(completeSignal);
};
});
this._queue.push({readySignal, transactionComplete});
this._processQueue();
return dbReady;
}
async _processQueue() {
// We're already processing the queue
if (this._inTransaction) {
return;
}
while (true) {
let item = this._queue.shift();
if (!item) {
return;
}
this._inTransaction = true;
item.readySignal();
await item.transactionComplete;
this._inTransaction = false;
}
}
}
/***/ })
/******/ });

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,6 @@
import { TransactionManager } from "./transaction_manager.mjs";
import { TaskManager } from "./task_manager.mjs";
import { Transaction } from "./transaction.mjs";
import { query } from "./query.mjs";
export class Database {
@ -8,7 +10,7 @@ export class Database {
this.version = null;
this.path = null;
this.transactionManager = new TransactionManager(dbName, this.adapter);
this.taskManager = new TaskManager();
}
async open() {
@ -32,10 +34,13 @@ export class Database {
}
transaction() {
return this.transactionManager.transaction();
return new Transaction(
this.dbName,
this.adapter,
this.taskManager.enqueue.bind(this.taskManager)
);
}
async autoTransaction(cb, inheritTx) {
let tx = inheritTx || this.transaction();
let result;
@ -57,8 +62,22 @@ export class Database {
}
// Helper method to start a transaction and execute a single SQL statement
sql(sql, ...args) {
return this.autoTransaction((tx) => tx.sql(sql, ...args));
// Execute a single SQL statement
async sql(sql, ...args) {
const completeCb = await this.taskManager.enqueue();
let result;
let error;
try {
result = await this.adapter.sql(this.dbName, ...query(sql, ...args));
} catch (e) {
error = e;
}
completeCb();
if (error) {
throw error;
}
return new Result(result);
}
}

46
src/query.mjs Normal file
View File

@ -0,0 +1,46 @@
export function query(sql, ...args) {
validateQuery(sql, ...args);
return buildQuery(sql, ...args);
}
// Allow for slightly more complex parameter substitution.
// Instances of "???" will be replaced by the same number of comma-separated
// question marks as items in the corresponding nested parateter array
// eg. buildQuery("SELECT (???) FROM ?", [["col1", "col2"], "table1"])
// would output: ["SELECT (?, ?) FROM ?", ["col1", "col2", "table1"]]
export function buildQuery(sql, ...args) {
const parts = sql.split("???");
const subParamLengths = args
.filter(Array.isArray)
.map((a) => a.length);
if (parts.length !== subParamLengths.length + 1) {
throw new PawSQLiteError("Unable to build query: sub-" +
"paramters do not match sub-paramters in query");
}
const newQuery = parts.reduce((p1, p2, i) => {
const length = subParamLengths[i - 1];
return p1 + new Array(length).fill("?").join(", ") + p2;
});
const flatParams = args.reduce((acc, v) => {
if (Array.isArray(v)) {
Array.prototype.push.apply(acc, v);
} else {
acc.push(v);
}
return acc;
}, []);
return [newQuery, ...flatParams];
}
export function validateQuery(sql, ...args) {
const reg = /^\s*(BEGIN|END|COMMIT|ROLLBACK)(?:[^A-Z]|$)/i;
const match = reg.exec(sql);
if (match) {
const statement = match[1].toUpperCase();
throw new PawSQLiteError("Manually managing transactions is " +
"forbidden. Found: \"" + statement + "\" statement.");
}
}

46
src/task_manager.mjs Normal file
View File

@ -0,0 +1,46 @@
export class TaskManager {
constructor() {
this._queue = [];
this._inTask = false;
}
enqueue() {
let completeSignal;
let taskComplete = new Promise((r, _) => {
completeSignal = r;
});
let readySignal;
let dbReady = new Promise((r, _) => {
readySignal = () => {
r(completeSignal);
};
});
this._queue.push({readySignal, taskComplete});
this._processQueue();
return dbReady;
}
async _processQueue() {
// We're already processing the queue
if (this._inTask) {
return;
}
while (true) {
let item = this._queue.shift();
if (!item) {
return;
}
this._inTask = true;
item.readySignal();
await item.taskComplete;
this._inTask = false;
}
}
}

View File

@ -1,5 +1,6 @@
import { Result } from "./result.mjs";
import { PawSQLiteError } from "./pawsqlite_error.mjs";
import { query } from "./query.mjs";
export class Transaction {
@ -25,46 +26,16 @@ export class Transaction {
await this._waitUntilReady();
}
let result;
try {
return await this._executeSQL(sql, ...args);
result = await this.adapter.sql(this.dbName, ...query(sql, ...args));
} catch (e) {
if (this._rollbackOnError) {
await this.rollback();
}
throw e;
}
}
// Allow for slightly more complex parameter substitution.
// Instances of "???" will be replaced by the same number of comma-separated
// question marks as items in the corresponding nested parateter array
// eg. buildQuery("SELECT (???) FROM ?", [["col1", "col2"], "table1"])
// would output: ["SELECT (?, ?) FROM ?", ["col1", "col2", "table1"]]
buildQuery(sql, ...args) {
const parts = sql.split("???");
const subParamLengths = args
.filter(Array.isArray)
.map((a) => a.length);
if (parts.length !== subParamLengths.length + 1) {
throw new PawSQLiteError("Unable to build query: sub-" +
"paramters do not match sub-paramters in query");
}
const newQuery = parts.reduce((p1, p2, i) => {
const length = subParamLengths[i - 1];
return p1 + new Array(length).fill("?").join(", ") + p2;
});
const flatParams = args.reduce((acc, v) => {
if (Array.isArray(v)) {
Array.prototype.push.apply(acc, v);
} else {
acc.push(v);
}
return acc;
}, []);
return [newQuery, ...flatParams];
return new Result(result);
}
commit() {
@ -87,21 +58,6 @@ export class Transaction {
await this._readyWait;
}
async _executeSQL(sql, ...args) {
const reg = /^\s*(BEGIN|END|COMMIT|ROLLBACK)(?:[^A-Z]|$)/i;
const match = reg.exec(sql);
if (match) {
const statement = match[1].toUpperCase();
throw new PawSQLiteError("Manually managing transactions is " +
"forbidden. Found: \"" + statement + "\" statement.");
}
const result = await this.adapter.sql(this.dbName,
...this.buildQuery(sql, ...args));
return new Result(result);
}
async _begin() {
const result = await this.adapter.sql(this.dbName, "BEGIN");
}

View File

@ -1,55 +0,0 @@
import { Transaction } from "./transaction.mjs";
export class TransactionManager {
constructor(dbName, adapter) {
this.dbName = dbName;
this.adapter = adapter;
this._queue = [];
this._inTransaction = false;
}
transaction() {
return new Transaction(this.dbName, this.adapter,
this.enqueue.bind(this));
}
enqueue() {
let completeSignal;
let transactionComplete = new Promise((r, _) => {
completeSignal = r;
});
let readySignal;
let dbReady = new Promise((r, _) => {
readySignal = () => {
r(completeSignal);
};
});
this._queue.push({readySignal, transactionComplete});
this._processQueue();
return dbReady;
}
async _processQueue() {
// We're already processing the queue
if (this._inTransaction) {
return;
}
while (true) {
let item = this._queue.shift();
if (!item) {
return;
}
this._inTransaction = true;
item.readySignal();
await item.transactionComplete;
this._inTransaction = false;
}
}
}