services_sql.js.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JSDoc: Source: services/sql.js</title>
  6. <script src="scripts/prettify/prettify.js"> </script>
  7. <script src="scripts/prettify/lang-css.js"> </script>
  8. <!--[if lt IE 9]>
  9. <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
  10. <![endif]-->
  11. <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
  12. <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
  13. </head>
  14. <body>
  15. <div id="main">
  16. <h1 class="page-title">Source: services/sql.js</h1>
  17. <section>
  18. <article>
  19. <pre class="prettyprint source linenums"><code>"use strict";
  20. /**
  21. * @module sql
  22. */
  23. const log = require('./log');
  24. const Database = require('better-sqlite3');
  25. const dataDir = require('./data_dir');
  26. const cls = require('./cls');
  27. const fs = require("fs-extra");
  28. const dbConnection = new Database(dataDir.DOCUMENT_PATH);
  29. dbConnection.pragma('journal_mode = WAL');
  30. const LOG_ALL_QUERIES = false;
  31. [`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
  32. process.on(eventType, () => {
  33. if (dbConnection) {
  34. // closing connection is especially important to fold -wal file into the main DB file
  35. // (see https://sqlite.org/tempfiles.html for details)
  36. dbConnection.close();
  37. }
  38. });
  39. });
  40. function insert(tableName, rec, replace = false) {
  41. const keys = Object.keys(rec || {});
  42. if (keys.length === 0) {
  43. log.error(`Can't insert empty object into table ${tableName}`);
  44. return;
  45. }
  46. const columns = keys.join(", ");
  47. const questionMarks = keys.map(p => "?").join(", ");
  48. const query = `INSERT
  49. ${replace ? "OR REPLACE" : ""} INTO
  50. ${tableName}
  51. (
  52. ${columns}
  53. )
  54. VALUES (${questionMarks})`;
  55. const res = execute(query, Object.values(rec));
  56. return res ? res.lastInsertRowid : null;
  57. }
  58. function replace(tableName, rec) {
  59. return insert(tableName, rec, true);
  60. }
  61. function upsert(tableName, primaryKey, rec) {
  62. const keys = Object.keys(rec || {});
  63. if (keys.length === 0) {
  64. log.error(`Can't upsert empty object into table ${tableName}`);
  65. return;
  66. }
  67. const columns = keys.join(", ");
  68. const questionMarks = keys.map(colName => `@${colName}`).join(", ");
  69. const updateMarks = keys.map(colName => `${colName} = @${colName}`).join(", ");
  70. const query = `INSERT INTO ${tableName} (${columns}) VALUES (${questionMarks})
  71. ON CONFLICT (${primaryKey}) DO UPDATE SET ${updateMarks}`;
  72. for (const idx in rec) {
  73. if (rec[idx] === true || rec[idx] === false) {
  74. rec[idx] = rec[idx] ? 1 : 0;
  75. }
  76. }
  77. execute(query, rec);
  78. }
  79. const statementCache = {};
  80. function stmt(sql) {
  81. if (!(sql in statementCache)) {
  82. statementCache[sql] = dbConnection.prepare(sql);
  83. }
  84. return statementCache[sql];
  85. }
  86. function getRow(query, params = []) {
  87. return wrap(query, s => s.get(params));
  88. }
  89. function getRowOrNull(query, params = []) {
  90. const all = getRows(query, params);
  91. return all.length > 0 ? all[0] : null;
  92. }
  93. function getValue(query, params = []) {
  94. return wrap(query, s => s.pluck().get(params));
  95. }
  96. // smaller values can result in better performance due to better usage of statement cache
  97. const PARAM_LIMIT = 100;
  98. function getManyRows(query, params) {
  99. let results = [];
  100. while (params.length > 0) {
  101. const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
  102. params = params.slice(curParams.length);
  103. const curParamsObj = {};
  104. let j = 1;
  105. for (const param of curParams) {
  106. curParamsObj['param' + j++] = param;
  107. }
  108. let i = 1;
  109. const questionMarks = curParams.map(() => ":param" + i++).join(",");
  110. const curQuery = query.replace(/\?\?\?/g, questionMarks);
  111. const statement = curParams.length === PARAM_LIMIT
  112. ? stmt(curQuery)
  113. : dbConnection.prepare(curQuery);
  114. const subResults = statement.all(curParamsObj);
  115. results = results.concat(subResults);
  116. }
  117. return results;
  118. }
  119. function getRows(query, params = []) {
  120. return wrap(query, s => s.all(params));
  121. }
  122. function getRawRows(query, params = []) {
  123. return wrap(query, s => s.raw().all(params));
  124. }
  125. function iterateRows(query, params = []) {
  126. if (LOG_ALL_QUERIES) {
  127. console.log(query);
  128. }
  129. return stmt(query).iterate(params);
  130. }
  131. function getMap(query, params = []) {
  132. const map = {};
  133. const results = getRawRows(query, params);
  134. for (const row of results) {
  135. map[row[0]] = row[1];
  136. }
  137. return map;
  138. }
  139. function getColumn(query, params = []) {
  140. return wrap(query, s => s.pluck().all(params));
  141. }
  142. function execute(query, params = []) {
  143. return wrap(query, s => s.run(params));
  144. }
  145. function executeMany(query, params) {
  146. if (LOG_ALL_QUERIES) {
  147. console.log(query);
  148. }
  149. while (params.length > 0) {
  150. const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
  151. params = params.slice(curParams.length);
  152. const curParamsObj = {};
  153. let j = 1;
  154. for (const param of curParams) {
  155. curParamsObj['param' + j++] = param;
  156. }
  157. let i = 1;
  158. const questionMarks = curParams.map(() => ":param" + i++).join(",");
  159. const curQuery = query.replace(/\?\?\?/g, questionMarks);
  160. dbConnection.prepare(curQuery).run(curParamsObj);
  161. }
  162. }
  163. function executeScript(query) {
  164. if (LOG_ALL_QUERIES) {
  165. console.log(query);
  166. }
  167. return dbConnection.exec(query);
  168. }
  169. function wrap(query, func) {
  170. const startTimestamp = Date.now();
  171. let result;
  172. if (LOG_ALL_QUERIES) {
  173. console.log(query);
  174. }
  175. try {
  176. result = func(stmt(query));
  177. }
  178. catch (e) {
  179. if (e.message.includes("The database connection is not open")) {
  180. // this often happens on killing the app which puts these alerts in front of user
  181. // in these cases error should be simply ignored.
  182. console.log(e.message);
  183. return null;
  184. }
  185. throw e;
  186. }
  187. const milliseconds = Date.now() - startTimestamp;
  188. if (milliseconds >= 20 &amp;&amp; !cls.isSlowQueryLoggingDisabled()) {
  189. if (query.includes("WITH RECURSIVE")) {
  190. log.info(`Slow recursive query took ${milliseconds}ms.`);
  191. }
  192. else {
  193. log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`);
  194. }
  195. }
  196. return result;
  197. }
  198. function transactional(func) {
  199. try {
  200. const ret = dbConnection.transaction(func).deferred();
  201. if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released)
  202. require('./ws').sendTransactionEntityChangesToAllClients();
  203. }
  204. return ret;
  205. }
  206. catch (e) {
  207. const entityChangeIds = cls.getAndClearEntityChangeIds();
  208. if (entityChangeIds.length > 0) {
  209. log.info("Transaction rollback dirtied the becca, forcing reload.");
  210. require('../becca/becca_loader').load();
  211. }
  212. // the maxEntityChangeId has been incremented during failed transaction, need to recalculate
  213. require('./entity_changes').recalculateMaxEntityChangeId();
  214. throw e;
  215. }
  216. }
  217. function fillParamList(paramIds, truncate = true) {
  218. if (paramIds.length === 0) {
  219. return;
  220. }
  221. if (truncate) {
  222. execute("DELETE FROM param_list");
  223. }
  224. paramIds = Array.from(new Set(paramIds));
  225. if (paramIds.length > 30000) {
  226. fillParamList(paramIds.slice(30000), false);
  227. paramIds = paramIds.slice(0, 30000);
  228. }
  229. // doing it manually to avoid this showing up on the sloq query list
  230. const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`);
  231. s.run(paramIds);
  232. }
  233. async function copyDatabase(targetFilePath) {
  234. try {
  235. fs.unlinkSync(targetFilePath);
  236. } catch (e) {
  237. } // unlink throws exception if the file did not exist
  238. await dbConnection.backup(targetFilePath);
  239. }
  240. function disableSlowQueryLogging(cb) {
  241. const orig = cls.isSlowQueryLoggingDisabled();
  242. try {
  243. cls.disableSlowQueryLogging(true);
  244. return cb();
  245. }
  246. finally {
  247. cls.disableSlowQueryLogging(orig);
  248. }
  249. }
  250. module.exports = {
  251. dbConnection,
  252. insert,
  253. replace,
  254. /**
  255. * Get single value from the given query - first column from first returned row.
  256. *
  257. * @method
  258. * @param {string} query - SQL query with ? used as parameter placeholder
  259. * @param {object[]} [params] - array of params if needed
  260. * @returns [object] - single value
  261. */
  262. getValue,
  263. /**
  264. * Get first returned row.
  265. *
  266. * @method
  267. * @param {string} query - SQL query with ? used as parameter placeholder
  268. * @param {object[]} [params] - array of params if needed
  269. * @returns {object} - map of column name to column value
  270. */
  271. getRow,
  272. getRowOrNull,
  273. /**
  274. * Get all returned rows.
  275. *
  276. * @method
  277. * @param {string} query - SQL query with ? used as parameter placeholder
  278. * @param {object[]} [params] - array of params if needed
  279. * @returns {object[]} - array of all rows, each row is a map of column name to column value
  280. */
  281. getRows,
  282. getRawRows,
  283. iterateRows,
  284. getManyRows,
  285. /**
  286. * Get a map of first column mapping to second column.
  287. *
  288. * @method
  289. * @param {string} query - SQL query with ? used as parameter placeholder
  290. * @param {object[]} [params] - array of params if needed
  291. * @returns {object} - map of first column to second column
  292. */
  293. getMap,
  294. /**
  295. * Get a first column in an array.
  296. *
  297. * @method
  298. * @param {string} query - SQL query with ? used as parameter placeholder
  299. * @param {object[]} [params] - array of params if needed
  300. * @returns {object[]} - array of first column of all returned rows
  301. */
  302. getColumn,
  303. /**
  304. * Execute SQL
  305. *
  306. * @method
  307. * @param {string} query - SQL query with ? used as parameter placeholder
  308. * @param {object[]} [params] - array of params if needed
  309. */
  310. execute,
  311. executeMany,
  312. executeScript,
  313. transactional,
  314. upsert,
  315. fillParamList,
  316. copyDatabase,
  317. disableSlowQueryLogging
  318. };
  319. </code></pre>
  320. </article>
  321. </section>
  322. </div>
  323. <nav>
  324. <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-sql.html">sql</a></li></ul><h3>Classes</h3><ul><li><a href="AbstractBeccaEntity.html">AbstractBeccaEntity</a></li><li><a href="BAttachment.html">BAttachment</a></li><li><a href="BAttribute.html">BAttribute</a></li><li><a href="BBranch.html">BBranch</a></li><li><a href="BEtapiToken.html">BEtapiToken</a></li><li><a href="BNote.html">BNote</a></li><li><a href="BOption.html">BOption</a></li><li><a href="BRecentNote.html">BRecentNote</a></li><li><a href="BRevision.html">BRevision</a></li><li><a href="BackendScriptApi.html">BackendScriptApi</a></li></ul><h3>Global</h3><ul><li><a href="global.html#api">api</a></li></ul>
  325. </nav>
  326. <br class="clear">
  327. <footer>
  328. Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.2</a>
  329. </footer>
  330. <script> prettyPrint(); </script>
  331. <script src="scripts/linenumber.js"> </script>
  332. </body>
  333. </html>