From 7387e20b093c94d5d8856dfd980e0e1f01c67ccb Mon Sep 17 00:00:00 2001 From: Ben Ashton Date: Tue, 16 Mar 2021 11:26:59 -0700 Subject: [PATCH] Added the ability to execute sql outside of transactions with the database.sql method --- README.md | 2 +- cjs/pawsqlite.js | 278 ++++++++++++++++++++---------------- cjs/pawsqlite.js.map | 2 +- src/database.mjs | 33 ++++- src/query.mjs | 46 ++++++ src/task_manager.mjs | 46 ++++++ src/transaction.mjs | 52 +------ src/transaction_manager.mjs | 55 ------- 8 files changed, 278 insertions(+), 236 deletions(-) create mode 100644 src/query.mjs create mode 100644 src/task_manager.mjs delete mode 100644 src/transaction_manager.mjs diff --git a/README.md b/README.md index 35a155e..72cbece 100644 --- a/README.md +++ b/README.md @@ -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"); diff --git a/cjs/pawsqlite.js b/cjs/pawsqlite.js index 3c4b75d..5138b53 100644 --- a/cjs/pawsqlite.js +++ b/cjs/pawsqlite.js @@ -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; - } - } -} - /***/ }) /******/ }); diff --git a/cjs/pawsqlite.js.map b/cjs/pawsqlite.js.map index bb6df23..bd11d40 100644 --- a/cjs/pawsqlite.js.map +++ b/cjs/pawsqlite.js.map @@ -1 +1 @@ -{"version":3,"sources":["webpack://pawsqlite/./src/adapter_wrapper.mjs","webpack://pawsqlite/./src/database.mjs","webpack://pawsqlite/./src/log.mjs","webpack://pawsqlite/./src/pawsqlite.mjs","webpack://pawsqlite/./src/pawsqlite_error.mjs","webpack://pawsqlite/./src/result.mjs","webpack://pawsqlite/./src/transaction.mjs","webpack://pawsqlite/./src/transaction_manager.mjs","webpack://pawsqlite/webpack/bootstrap","webpack://pawsqlite/webpack/runtime/define property getters","webpack://pawsqlite/webpack/runtime/hasOwnProperty shorthand","webpack://pawsqlite/webpack/runtime/make namespace object","webpack://pawsqlite/./src/pawsqlite_cjs.mjs"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAuD;;;AAGhD;AACP;;AAEA;AACA;AACA,gBAAgB,gEAAc,uCAAuC,OAAO;AAC5E;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,gEAAc;AAC9B;AACA;;AAEA;AACA;AACA;AACA;AACA,SAAS;AACT,gBAAgB,qEAAmB;AACnC;AACA;AACA,KAAK;AACL;AACA;AACA,GAAG;;AAEH;AACA,C;;;;;;;;;;;;;;;ACtC+D;;;AAGxD;AACP;AACA;AACA;AACA;AACA;;AAEA,kCAAkC,wEAAkB;AACpD;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;AACA;;AAEA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;;AAGA;AACA;AACA;AACA;AACA,C;;;;;;;;;;;;;;;AC/DA;;AAEO;AACP;AACA;AACA,G;AACA;;AAEO;AACP;AACA;;AAEA;AACA;AACA;AACA;AACA,C;;;;;;;;;;;;;;;;;;;AChB0C;AACU;AACG;AACV;;AAEU;;;AAGvD;;AAEO;AACP,2CAA2C;AAC3C;;AAEA,iBAAiB,mDAAQ;AACzB;AACA;AACA,GAAG;AACH,uCAAuC;AACvC;;AAEA;AACA,GAAG;AACH;AACA;AACA,gBAAgB,gEAAc;AAC9B;;AAEA,oBAAoB,iEAAW;;AAE/B;AACA,gBAAgB,gEAAc,8BAA8B,eAAe;AAC3E;AACA;;AAEA,IAAI,6CAAG,wBAAwB,eAAe;;AAE9C;AACA,GAAG;AACH,SAAS,iDAAW;AACpB;;;;AAIA;AACA;;AAEA;AACA,cAAc,gEAAc,qBAAqB,YAAY;AAC7D;;AAEA;AACA,C;;;;;;;;;;;;;;ACpDO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,C;;;;;;;;;;;;;;;ACXuD;;;AAGhD;AACP;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;;;;;;;;;;;;;;;;AClBsC;AACiB;;;AAGhD;AACP;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAgB,gEAAc;AAC9B;;AAEA;AACA;AACA;;AAEA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,gEAAc;AAC9B;AACA;;AAEA;AACA;AACA;AACA,KAAK;;AAEL;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA,KAAK;;AAEL;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,gBAAgB,gEAAc;AAC9B;AACA;;AAEA;AACA;;AAEA,eAAe,+CAAM;AACrB;;AAEA;AACA;AACA;;AAEA;AACA;AACA,gBAAgB,gEAAc;AAC9B;AACA;;AAEA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA,eAAe,+CAAM;AACrB;AACA,C;;;;;;;;;;;;;;;ACzIgD;;AAEzC;AACP;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,eAAe,yDAAW;AAC1B;AACA;;AAEA;AACA;AACA;AACA;AACA,KAAK;;AAEL;AACA;AACA;AACA;AACA;AACA,KAAK;;AAEL,sBAAsB,iCAAiC;;AAEvD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,C;;;;;;UCtDA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCrBA;WACA;WACA;WACA;WACA,wCAAwC,yCAAyC;WACjF;WACA;WACA,E;;;;;WCPA,wF;;;;;WCAA;WACA;WACA;WACA,sDAAsD,kBAAkB;WACxE;WACA,+CAA+C,cAAc;WAC7D,E;;;;;;;;;;;;;;;;ACN4C;AACW;;AAEvD,iEAAe,cAAc,qDAAS;AACtC;AACA,WAAW,gEAAc;AACzB;AACA,CAAC,CAAC,E","file":"pawsqlite.js","sourcesContent":["import { PawSQLiteError } from \"./pawsqlite_error.mjs\";\n\n\nexport function wrapAdapter(adapter) {\n const wrapped = {};\n\n [\"name\", \"open\", \"close\", \"delete\", \"sql\"].forEach((prop) => {\n if (!(prop in adapter)) {\n throw new PawSQLiteError(`Invalid adapter: missing property: ${ prop }`);\n }\n\n if (\n prop === \"name\" && !(\n typeof adapter[prop] === \"string\" ||\n adapter[prop] instanceof String\n ) ||\n prop !== \"name\" && (\n typeof adapter[prop] !== \"function\"\n )\n ) {\n throw new PawSQLiteError(\"Invalid adapter: invalid type for property: \" +\n prop);\n }\n\n if (typeof adapter[prop] === \"function\") {\n wrapped[prop] = async (...args) => {\n try {\n return await adapter[prop](...args);\n } catch (err) {\n throw PawSQLiteError.from(err);\n }\n };\n } else {\n wrapped[prop] = adapter[prop];\n }\n });\n\n return wrapped;\n}","import { TransactionManager } from \"./transaction_manager.mjs\";\n\n\nexport class Database {\n constructor(dbName, adapter) {\n this.dbName = dbName;\n this.adapter = adapter;\n this.version = null;\n this.path = null;\n\n this.transactionManager = new TransactionManager(dbName, this.adapter);\n }\n\n async open() {\n let response = await this.adapter.open(this.dbName);\n\n if (response) {\n if (response.hasOwnProperty('version')) {\n this.version = response.version;\n }\n if (response.hasOwnProperty('path')) {\n this.path = response.path;\n }\n }\n\n // Allow chaining\n return this;\n }\n\n close() {\n return this.adapter.close(this.dbName);\n }\n\n transaction() {\n return this.transactionManager.transaction();\n }\n\n\n async autoTransaction(cb, inheritTx) {\n let tx = inheritTx || this.transaction();\n let result;\n\n try {\n result = await cb(tx);\n } catch (e) {\n if (!inheritTx) {\n await tx.rollback();\n }\n throw e;\n }\n\n if (!inheritTx) {\n await tx.commit();\n }\n\n return result;\n }\n\n\n // Helper method to start a transaction and execute a single SQL statement\n sql(sql, ...args) {\n return this.autoTransaction((tx) => tx.sql(sql, ...args));\n }\n}","let DEBUG = false;\n\nexport function log(...args) {\n if (DEBUG) {\n console.log(...args);\n } \n}\n\nexport function enableDebug(active) {\n DEBUG = !!active;\n log(\"PawSQLite: debugging \" + (DEBUG ? \"enabled\" : \"disabled\"));\n\n if (DEBUG) {\n log(\"You might also want to enable debugging for the adapter that you \" +\n \"are using\");\n }\n}","import { Database } from \"./database.mjs\";\nimport { wrapAdapter } from \"./adapter_wrapper.mjs\";\nimport { PawSQLiteError } from \"./pawsqlite_error.mjs\";\nimport { enableDebug, log } from \"./log.mjs\";\n\nexport { PawSQLiteError } from \"./pawsqlite_error.mjs\";\n\n\nconst adapters = [];\n\nexport const PawSQLite = {\n open: async function(dbName, options = {}) {\n const adapter = getAdapter(options.adapter);\n\n let db = new Database(dbName, adapter);\n await db.open();\n return db;\n },\n delete: function(dbName, options = {}) {\n const adapter = getAdapter(options.adapter);\n\n return adapter.delete(dbName);\n },\n registerAdapter: function(adapter) {\n if (!adapter) {\n throw new PawSQLiteError(\"Invalid adapter\");\n }\n\n const wrapped = wrapAdapter(adapter);\n\n if (getAdapter(wrapped.name, true)) {\n throw new PawSQLiteError(`An adapter with the name: ${ wrapped.name } ` +\n `Already exists`);\n }\n\n log(`Registered adapter: ${ wrapped.name }`);\n\n adapters.push(wrapped);\n },\n debug: enableDebug\n};\n\n\n\nfunction getAdapter(adapterName, silent=false) {\n const adapter = adapters.find((a) => a.name === adapterName);\n\n if (!(silent || adapter)) {\n throw new PawSQLiteError(`Unknown Adapter: ${adapterName}`);\n }\n\n return adapter;\n}","export class PawSQLiteError extends Error {\n static from(err) {\n const p_err = new PawSQLiteError(err.toString());\n p_err.cause = err;\n return p_err;\n }\n\n constructor(message) {\n super(message);\n this.name = \"PawSQLiteError\";\n }\n}","import { PawSQLiteError } from \"./pawsqlite_error.mjs\";\n\n\nexport class Result extends Array {\n constructor(result) {\n if (result.hasOwnProperty(\"rows\")) {\n super(...result.rows);\n } else {\n super();\n }\n\n if (result.hasOwnProperty(\"insertId\")) {\n this.insertId = result.insertId;\n }\n if (result.hasOwnProperty(\"rowsAffected\")) {\n this.rowsAffected = result.rowsAffected;\n }\n }\n}","import { Result } from \"./result.mjs\";\nimport { PawSQLiteError } from \"./pawsqlite_error.mjs\";\n\n\nexport class Transaction {\n constructor(dbName, adapter, enqueue, rollbackOnError=false) {\n this.dbName = dbName;\n this.adapter = adapter;\n this._rollbackOnError = rollbackOnError;\n\n this._enqueue = enqueue;\n this._completeCb = null;\n\n this._readyWait = null;\n this._ready = false;\n this._finalized = false;\n }\n\n async sql(sql, ...args) {\n if (this._finalized) {\n throw new PawSQLiteError(\"Transaction has already completed\");\n }\n\n if (!this._ready) {\n await this._waitUntilReady();\n }\n\n try {\n return await this._executeSQL(sql, ...args);\n } catch (e) {\n if (this._rollbackOnError) {\n await this.rollback();\n }\n throw e;\n }\n }\n\n // Allow for slightly more complex parameter substitution.\n // Instances of \"???\" will be replaced by the same number of comma-separated\n // question marks as items in the corresponding nested parateter array\n // eg. buildQuery(\"SELECT (???) FROM ?\", [[\"col1\", \"col2\"], \"table1\"])\n // would output: [\"SELECT (?, ?) FROM ?\", [\"col1\", \"col2\", \"table1\"]]\n buildQuery(sql, ...args) {\n const parts = sql.split(\"???\");\n const subParamLengths = args\n .filter(Array.isArray)\n .map((a) => a.length);\n if (parts.length !== subParamLengths.length + 1) {\n throw new PawSQLiteError(\"Unable to build query: sub-\" +\n \"paramters do not match sub-paramters in query\");\n }\n\n const newQuery = parts.reduce((p1, p2, i) => {\n const length = subParamLengths[i - 1];\n return p1 + new Array(length).fill(\"?\").join(\", \") + p2;\n });\n\n const flatParams = args.reduce((acc, v) => {\n if (Array.isArray(v)) {\n Array.prototype.push.apply(acc, v);\n } else {\n acc.push(v);\n }\n return acc;\n }, []);\n\n return [newQuery, ...flatParams];\n }\n\n commit() {\n return this._complete(\"COMMIT\");\n }\n\n rollback() {\n return this._complete(\"ROLLBACK\");\n }\n\n async _waitUntilReady() {\n if (!this._readyWait) {\n this._readyWait = (async () => {\n this._completeCb = await this._enqueue();\n await this._begin();\n this._ready = true;\n })();\n }\n \n await this._readyWait;\n }\n\n async _executeSQL(sql, ...args) {\n const reg = /^\\s*(BEGIN|END|COMMIT|ROLLBACK)(?:[^A-Z]|$)/i;\n const match = reg.exec(sql);\n if (match) {\n const statement = match[1].toUpperCase();\n throw new PawSQLiteError(\"Manually managing transactions is \" +\n \"forbidden. Found: \\\"\" + statement + \"\\\" statement.\");\n }\n\n const result = await this.adapter.sql(this.dbName,\n ...this.buildQuery(sql, ...args));\n\n return new Result(result);\n }\n\n async _begin() {\n const result = await this.adapter.sql(this.dbName, \"BEGIN\");\n }\n\n async _complete(sql) {\n if (this._finalized) {\n throw new PawSQLiteError(\"Transaction has already completed\");\n }\n this._finalized = true;\n\n if (!this._readyWait) {\n // Transaction was unused\n return;\n } else if (!this._ready) {\n await this._waitUntilReady();\n }\n\n let result;\n let error;\n try {\n result = await this.adapter.sql(this.dbName, sql);\n } catch (e) {\n error = e;\n }\n \n this._completeCb();\n \n if (error) {\n throw error;\n }\n\n return new Result(result);\n }\n}","import { Transaction } from \"./transaction.mjs\";\n\nexport class TransactionManager {\n constructor(dbName, adapter) {\n this.dbName = dbName;\n this.adapter = adapter;\n this._queue = [];\n this._inTransaction = false;\n }\n\n transaction() {\n return new Transaction(this.dbName, this.adapter,\n this.enqueue.bind(this));\n }\n\n enqueue() {\n let completeSignal;\n let transactionComplete = new Promise((r, _) => {\n completeSignal = r;\n });\n\n let readySignal;\n let dbReady = new Promise((r, _) => {\n readySignal = () => {\n r(completeSignal);\n };\n });\n\n this._queue.push({readySignal, transactionComplete});\n\n this._processQueue();\n return dbReady;\n }\n\n async _processQueue() {\n // We're already processing the queue\n if (this._inTransaction) {\n return;\n }\n\n while (true) {\n let item = this._queue.shift();\n if (!item) {\n return;\n }\n\n this._inTransaction = true;\n\n item.readySignal();\n await item.transactionComplete;\n\n this._inTransaction = false;\n }\n }\n}","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tif(__webpack_module_cache__[moduleId]) {\n\t\treturn __webpack_module_cache__[moduleId].exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import { PawSQLite } from \"./pawsqlite.mjs\";\nimport { PawSQLiteError } from \"./pawsqlite_error.mjs\";\n\nexport default Object.create(PawSQLite, {\n Error: {\n value: PawSQLiteError\n }\n});"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack://pawsqlite/./src/adapter_wrapper.mjs","webpack://pawsqlite/./src/database.mjs","webpack://pawsqlite/./src/log.mjs","webpack://pawsqlite/./src/pawsqlite.mjs","webpack://pawsqlite/./src/pawsqlite_error.mjs","webpack://pawsqlite/./src/query.mjs","webpack://pawsqlite/./src/result.mjs","webpack://pawsqlite/./src/task_manager.mjs","webpack://pawsqlite/./src/transaction.mjs","webpack://pawsqlite/webpack/bootstrap","webpack://pawsqlite/webpack/runtime/define property getters","webpack://pawsqlite/webpack/runtime/hasOwnProperty shorthand","webpack://pawsqlite/webpack/runtime/make namespace object","webpack://pawsqlite/./src/pawsqlite_cjs.mjs"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAuD;;;AAGhD;AACP;;AAEA;AACA;AACA,gBAAgB,gEAAc,uCAAuC,OAAO;AAC5E;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,gEAAc;AAC9B;AACA;;AAEA;AACA;AACA;AACA;AACA,SAAS;AACT,gBAAgB,qEAAmB;AACnC;AACA;AACA,KAAK;AACL;AACA;AACA,GAAG;;AAEH;AACA,C;;;;;;;;;;;;;;;;;ACtCiD;AACD;AACZ;;;AAG7B;AACP;AACA;AACA;AACA;AACA;;AAEA,2BAA2B,0DAAW;AACtC;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,eAAe,yDAAW;AAC1B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;;AAGA;AACA;AACA;;AAEA;AACA;AACA;AACA,sDAAsD,iDAAK;AAC3D,KAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,C;;;;;;;;;;;;;;;AClFA;;AAEO;AACP;AACA;AACA,G;AACA;;AAEO;AACP;AACA;;AAEA;AACA;AACA;AACA;AACA,C;;;;;;;;;;;;;;;;;;;AChB0C;AACU;AACG;AACV;;AAEU;;;AAGvD;;AAEO;AACP,2CAA2C;AAC3C;;AAEA,iBAAiB,mDAAQ;AACzB;AACA;AACA,GAAG;AACH,uCAAuC;AACvC;;AAEA;AACA,GAAG;AACH;AACA;AACA,gBAAgB,gEAAc;AAC9B;;AAEA,oBAAoB,iEAAW;;AAE/B;AACA,gBAAgB,gEAAc,8BAA8B,eAAe;AAC3E;AACA;;AAEA,IAAI,6CAAG,wBAAwB,eAAe;;AAE9C;AACA,GAAG;AACH,SAAS,iDAAW;AACpB;;;;AAIA;AACA;;AAEA;AACA,cAAc,gEAAc,qBAAqB,YAAY;AAC7D;;AAEA;AACA,C;;;;;;;;;;;;;;ACpDO;AACP;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,C;;;;;;;;;;;;;;;;ACXO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,GAAG;;AAEH;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AC7CuD;;;AAGhD;AACP;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;;;;;;;;;;;;;;AClBO;AACP;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,KAAK;;AAEL;AACA;AACA;AACA;AACA;AACA,KAAK;;AAEL,sBAAsB,0BAA0B;;AAEhD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,C;;;;;;;;;;;;;;;;;AC7CsC;AACiB;AACnB;;;AAG7B;AACP;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,gBAAgB,gEAAc;AAC9B;;AAEA;AACA;AACA;;AAEA;AACA;AACA,sDAAsD,iDAAK;AAC3D,KAAK;AACL;AACA;AACA;AACA;AACA;AACA,eAAe,+CAAM;AACrB;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,gBAAgB,gEAAc;AAC9B;AACA;;AAEA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA,eAAe,+CAAM;AACrB;AACA,C;;;;;;UC7FA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCrBA;WACA;WACA;WACA;WACA,wCAAwC,yCAAyC;WACjF;WACA;WACA,E;;;;;WCPA,wF;;;;;WCAA;WACA;WACA;WACA,sDAAsD,kBAAkB;WACxE;WACA,+CAA+C,cAAc;WAC7D,E;;;;;;;;;;;;;;;;ACN4C;AACW;;AAEvD,iEAAe,cAAc,qDAAS;AACtC;AACA,WAAW,gEAAc;AACzB;AACA,CAAC,CAAC,E","file":"pawsqlite.js","sourcesContent":["import { PawSQLiteError } from \"./pawsqlite_error.mjs\";\n\n\nexport function wrapAdapter(adapter) {\n const wrapped = {};\n\n [\"name\", \"open\", \"close\", \"delete\", \"sql\"].forEach((prop) => {\n if (!(prop in adapter)) {\n throw new PawSQLiteError(`Invalid adapter: missing property: ${ prop }`);\n }\n\n if (\n prop === \"name\" && !(\n typeof adapter[prop] === \"string\" ||\n adapter[prop] instanceof String\n ) ||\n prop !== \"name\" && (\n typeof adapter[prop] !== \"function\"\n )\n ) {\n throw new PawSQLiteError(\"Invalid adapter: invalid type for property: \" +\n prop);\n }\n\n if (typeof adapter[prop] === \"function\") {\n wrapped[prop] = async (...args) => {\n try {\n return await adapter[prop](...args);\n } catch (err) {\n throw PawSQLiteError.from(err);\n }\n };\n } else {\n wrapped[prop] = adapter[prop];\n }\n });\n\n return wrapped;\n}","import { TaskManager } from \"./task_manager.mjs\";\nimport { Transaction } from \"./transaction.mjs\";\nimport { query } from \"./query.mjs\";\n\n\nexport class Database {\n constructor(dbName, adapter) {\n this.dbName = dbName;\n this.adapter = adapter;\n this.version = null;\n this.path = null;\n\n this.taskManager = new TaskManager();\n }\n\n async open() {\n let response = await this.adapter.open(this.dbName);\n\n if (response) {\n if (response.hasOwnProperty('version')) {\n this.version = response.version;\n }\n if (response.hasOwnProperty('path')) {\n this.path = response.path;\n }\n }\n\n // Allow chaining\n return this;\n }\n\n close() {\n return this.adapter.close(this.dbName);\n }\n\n transaction() {\n return new Transaction(\n this.dbName,\n this.adapter,\n this.taskManager.enqueue.bind(this.taskManager)\n );\n }\n\n async autoTransaction(cb, inheritTx) {\n let tx = inheritTx || this.transaction();\n let result;\n\n try {\n result = await cb(tx);\n } catch (e) {\n if (!inheritTx) {\n await tx.rollback();\n }\n throw e;\n }\n\n if (!inheritTx) {\n await tx.commit();\n }\n\n return result;\n }\n\n\n // Execute a single SQL statement\n async sql(sql, ...args) {\n const completeCb = await this.taskManager.enqueue();\n\n let result;\n let error;\n try {\n result = await this.adapter.sql(this.dbName, ...query(sql, ...args));\n } catch (e) {\n error = e;\n }\n completeCb();\n\n if (error) {\n throw error;\n }\n return new Result(result);\n }\n}","let DEBUG = false;\n\nexport function log(...args) {\n if (DEBUG) {\n console.log(...args);\n } \n}\n\nexport function enableDebug(active) {\n DEBUG = !!active;\n log(\"PawSQLite: debugging \" + (DEBUG ? \"enabled\" : \"disabled\"));\n\n if (DEBUG) {\n log(\"You might also want to enable debugging for the adapter that you \" +\n \"are using\");\n }\n}","import { Database } from \"./database.mjs\";\nimport { wrapAdapter } from \"./adapter_wrapper.mjs\";\nimport { PawSQLiteError } from \"./pawsqlite_error.mjs\";\nimport { enableDebug, log } from \"./log.mjs\";\n\nexport { PawSQLiteError } from \"./pawsqlite_error.mjs\";\n\n\nconst adapters = [];\n\nexport const PawSQLite = {\n open: async function(dbName, options = {}) {\n const adapter = getAdapter(options.adapter);\n\n let db = new Database(dbName, adapter);\n await db.open();\n return db;\n },\n delete: function(dbName, options = {}) {\n const adapter = getAdapter(options.adapter);\n\n return adapter.delete(dbName);\n },\n registerAdapter: function(adapter) {\n if (!adapter) {\n throw new PawSQLiteError(\"Invalid adapter\");\n }\n\n const wrapped = wrapAdapter(adapter);\n\n if (getAdapter(wrapped.name, true)) {\n throw new PawSQLiteError(`An adapter with the name: ${ wrapped.name } ` +\n `Already exists`);\n }\n\n log(`Registered adapter: ${ wrapped.name }`);\n\n adapters.push(wrapped);\n },\n debug: enableDebug\n};\n\n\n\nfunction getAdapter(adapterName, silent=false) {\n const adapter = adapters.find((a) => a.name === adapterName);\n\n if (!(silent || adapter)) {\n throw new PawSQLiteError(`Unknown Adapter: ${adapterName}`);\n }\n\n return adapter;\n}","export class PawSQLiteError extends Error {\n static from(err) {\n const p_err = new PawSQLiteError(err.toString());\n p_err.cause = err;\n return p_err;\n }\n\n constructor(message) {\n super(message);\n this.name = \"PawSQLiteError\";\n }\n}","export function query(sql, ...args) {\n validateQuery(sql, ...args);\n return buildQuery(sql, ...args);\n}\n\n// Allow for slightly more complex parameter substitution.\n// Instances of \"???\" will be replaced by the same number of comma-separated\n// question marks as items in the corresponding nested parateter array\n// eg. buildQuery(\"SELECT (???) FROM ?\", [[\"col1\", \"col2\"], \"table1\"])\n// would output: [\"SELECT (?, ?) FROM ?\", [\"col1\", \"col2\", \"table1\"]]\nexport function buildQuery(sql, ...args) {\n const parts = sql.split(\"???\");\n const subParamLengths = args\n .filter(Array.isArray)\n .map((a) => a.length);\n if (parts.length !== subParamLengths.length + 1) {\n throw new PawSQLiteError(\"Unable to build query: sub-\" +\n \"paramters do not match sub-paramters in query\");\n }\n\n const newQuery = parts.reduce((p1, p2, i) => {\n const length = subParamLengths[i - 1];\n return p1 + new Array(length).fill(\"?\").join(\", \") + p2;\n });\n\n const flatParams = args.reduce((acc, v) => {\n if (Array.isArray(v)) {\n Array.prototype.push.apply(acc, v);\n } else {\n acc.push(v);\n }\n return acc;\n }, []);\n\n return [newQuery, ...flatParams];\n}\n\nexport function validateQuery(sql, ...args) {\n const reg = /^\\s*(BEGIN|END|COMMIT|ROLLBACK)(?:[^A-Z]|$)/i;\n const match = reg.exec(sql);\n if (match) {\n const statement = match[1].toUpperCase();\n throw new PawSQLiteError(\"Manually managing transactions is \" +\n \"forbidden. Found: \\\"\" + statement + \"\\\" statement.\");\n }\n}\n","import { PawSQLiteError } from \"./pawsqlite_error.mjs\";\n\n\nexport class Result extends Array {\n constructor(result) {\n if (result.hasOwnProperty(\"rows\")) {\n super(...result.rows);\n } else {\n super();\n }\n\n if (result.hasOwnProperty(\"insertId\")) {\n this.insertId = result.insertId;\n }\n if (result.hasOwnProperty(\"rowsAffected\")) {\n this.rowsAffected = result.rowsAffected;\n }\n }\n}","export class TaskManager {\n constructor() {\n this._queue = [];\n this._inTask = false;\n }\n\n enqueue() {\n let completeSignal;\n let taskComplete = new Promise((r, _) => {\n completeSignal = r;\n });\n\n let readySignal;\n let dbReady = new Promise((r, _) => {\n readySignal = () => {\n r(completeSignal);\n };\n });\n\n this._queue.push({readySignal, taskComplete});\n\n this._processQueue();\n return dbReady;\n }\n\n async _processQueue() {\n // We're already processing the queue\n if (this._inTask) {\n return;\n }\n\n while (true) {\n let item = this._queue.shift();\n if (!item) {\n return;\n }\n\n this._inTask = true;\n\n item.readySignal();\n await item.taskComplete;\n\n this._inTask = false;\n }\n }\n}","import { Result } from \"./result.mjs\";\nimport { PawSQLiteError } from \"./pawsqlite_error.mjs\";\nimport { query } from \"./query.mjs\";\n\n\nexport class Transaction {\n constructor(dbName, adapter, enqueue, rollbackOnError=false) {\n this.dbName = dbName;\n this.adapter = adapter;\n this._rollbackOnError = rollbackOnError;\n\n this._enqueue = enqueue;\n this._completeCb = null;\n\n this._readyWait = null;\n this._ready = false;\n this._finalized = false;\n }\n\n async sql(sql, ...args) {\n if (this._finalized) {\n throw new PawSQLiteError(\"Transaction has already completed\");\n }\n\n if (!this._ready) {\n await this._waitUntilReady();\n }\n\n let result;\n try {\n result = await this.adapter.sql(this.dbName, ...query(sql, ...args));\n } catch (e) {\n if (this._rollbackOnError) {\n await this.rollback();\n }\n throw e;\n }\n return new Result(result);\n }\n\n commit() {\n return this._complete(\"COMMIT\");\n }\n\n rollback() {\n return this._complete(\"ROLLBACK\");\n }\n\n async _waitUntilReady() {\n if (!this._readyWait) {\n this._readyWait = (async () => {\n this._completeCb = await this._enqueue();\n await this._begin();\n this._ready = true;\n })();\n }\n \n await this._readyWait;\n }\n\n async _begin() {\n const result = await this.adapter.sql(this.dbName, \"BEGIN\");\n }\n\n async _complete(sql) {\n if (this._finalized) {\n throw new PawSQLiteError(\"Transaction has already completed\");\n }\n this._finalized = true;\n\n if (!this._readyWait) {\n // Transaction was unused\n return;\n } else if (!this._ready) {\n await this._waitUntilReady();\n }\n\n let result;\n let error;\n try {\n result = await this.adapter.sql(this.dbName, sql);\n } catch (e) {\n error = e;\n }\n \n this._completeCb();\n \n if (error) {\n throw error;\n }\n\n return new Result(result);\n }\n}","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tif(__webpack_module_cache__[moduleId]) {\n\t\treturn __webpack_module_cache__[moduleId].exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import { PawSQLite } from \"./pawsqlite.mjs\";\nimport { PawSQLiteError } from \"./pawsqlite_error.mjs\";\n\nexport default Object.create(PawSQLite, {\n Error: {\n value: PawSQLiteError\n }\n});"],"sourceRoot":""} \ No newline at end of file diff --git a/src/database.mjs b/src/database.mjs index dbcecd9..aedd1bb 100644 --- a/src/database.mjs +++ b/src/database.mjs @@ -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); } } \ No newline at end of file diff --git a/src/query.mjs b/src/query.mjs new file mode 100644 index 0000000..b6a1945 --- /dev/null +++ b/src/query.mjs @@ -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."); + } +} diff --git a/src/task_manager.mjs b/src/task_manager.mjs new file mode 100644 index 0000000..afd22aa --- /dev/null +++ b/src/task_manager.mjs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/transaction.mjs b/src/transaction.mjs index 888750f..3876a49 100644 --- a/src/transaction.mjs +++ b/src/transaction.mjs @@ -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"); } diff --git a/src/transaction_manager.mjs b/src/transaction_manager.mjs deleted file mode 100644 index 9c31be9..0000000 --- a/src/transaction_manager.mjs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file