services_sql.js.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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 dbConnection = new Database(dataDir.DOCUMENT_PATH);
  28. dbConnection.pragma('journal_mode = WAL');
  29. [`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
  30. process.on(eventType, () => {
  31. if (dbConnection) {
  32. // closing connection is especially important to fold -wal file into the main DB file
  33. // (see https://sqlite.org/tempfiles.html for details)
  34. dbConnection.close();
  35. }
  36. });
  37. });
  38. function insert(tableName, rec, replace = false) {
  39. const keys = Object.keys(rec);
  40. if (keys.length === 0) {
  41. log.error("Can't insert empty object into table " + tableName);
  42. return;
  43. }
  44. const columns = keys.join(", ");
  45. const questionMarks = keys.map(p => "?").join(", ");
  46. const query = "INSERT " + (replace ? "OR REPLACE" : "") + " INTO " + tableName + "(" + columns + ") VALUES (" + questionMarks + ")";
  47. const res = execute(query, Object.values(rec));
  48. return res ? res.lastInsertRowid : null;
  49. }
  50. function replace(tableName, rec) {
  51. return insert(tableName, rec, true);
  52. }
  53. function upsert(tableName, primaryKey, rec) {
  54. const keys = Object.keys(rec);
  55. if (keys.length === 0) {
  56. log.error("Can't upsert empty object into table " + tableName);
  57. return;
  58. }
  59. const columns = keys.join(", ");
  60. const questionMarks = keys.map(colName => "@" + colName).join(", ");
  61. const updateMarks = keys.map(colName => `${colName} = @${colName}`).join(", ");
  62. const query = `INSERT INTO ${tableName} (${columns}) VALUES (${questionMarks})
  63. ON CONFLICT (${primaryKey}) DO UPDATE SET ${updateMarks}`;
  64. for (const idx in rec) {
  65. if (rec[idx] === true || rec[idx] === false) {
  66. rec[idx] = rec[idx] ? 1 : 0;
  67. }
  68. }
  69. execute(query, rec);
  70. }
  71. const statementCache = {};
  72. function stmt(sql) {
  73. if (!(sql in statementCache)) {
  74. statementCache[sql] = dbConnection.prepare(sql);
  75. }
  76. return statementCache[sql];
  77. }
  78. function getRow(query, params = []) {
  79. return wrap(query, s => s.get(params));
  80. }
  81. function getRowOrNull(query, params = []) {
  82. const all = getRows(query, params);
  83. return all.length > 0 ? all[0] : null;
  84. }
  85. function getValue(query, params = []) {
  86. const row = getRowOrNull(query, params);
  87. if (!row) {
  88. return null;
  89. }
  90. return row[Object.keys(row)[0]];
  91. }
  92. // smaller values can result in better performance due to better usage of statement cache
  93. const PARAM_LIMIT = 100;
  94. function getManyRows(query, params) {
  95. let results = [];
  96. while (params.length > 0) {
  97. const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
  98. params = params.slice(curParams.length);
  99. const curParamsObj = {};
  100. let j = 1;
  101. for (const param of curParams) {
  102. curParamsObj['param' + j++] = param;
  103. }
  104. let i = 1;
  105. const questionMarks = curParams.map(() => ":param" + i++).join(",");
  106. const curQuery = query.replace(/\?\?\?/g, questionMarks);
  107. const statement = curParams.length === PARAM_LIMIT
  108. ? stmt(curQuery)
  109. : dbConnection.prepare(curQuery);
  110. const subResults = statement.all(curParamsObj);
  111. results = results.concat(subResults);
  112. }
  113. return results;
  114. }
  115. function getRows(query, params = []) {
  116. return wrap(query, s => s.all(params));
  117. }
  118. function getRawRows(query, params = []) {
  119. return wrap(query, s => s.raw().all(params));
  120. }
  121. function iterateRows(query, params = []) {
  122. return stmt(query).iterate(params);
  123. }
  124. function getMap(query, params = []) {
  125. const map = {};
  126. const results = getRows(query, params);
  127. for (const row of results) {
  128. const keys = Object.keys(row);
  129. map[row[keys[0]]] = row[keys[1]];
  130. }
  131. return map;
  132. }
  133. function getColumn(query, params = []) {
  134. const list = [];
  135. const result = getRows(query, params);
  136. if (result.length === 0) {
  137. return list;
  138. }
  139. const key = Object.keys(result[0])[0];
  140. for (const row of result) {
  141. list.push(row[key]);
  142. }
  143. return list;
  144. }
  145. function execute(query, params = []) {
  146. return wrap(query, s => s.run(params));
  147. }
  148. function executeWithoutTransaction(query, params = []) {
  149. dbConnection.run(query, params);
  150. }
  151. function executeMany(query, params) {
  152. while (params.length > 0) {
  153. const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
  154. params = params.slice(curParams.length);
  155. const curParamsObj = {};
  156. let j = 1;
  157. for (const param of curParams) {
  158. curParamsObj['param' + j++] = param;
  159. }
  160. let i = 1;
  161. const questionMarks = curParams.map(() => ":param" + i++).join(",");
  162. const curQuery = query.replace(/\?\?\?/g, questionMarks);
  163. dbConnection.prepare(curQuery).run(curParamsObj);
  164. }
  165. }
  166. function executeScript(query) {
  167. return dbConnection.exec(query);
  168. }
  169. function wrap(query, func) {
  170. const startTimestamp = Date.now();
  171. let result;
  172. try {
  173. result = func(stmt(query));
  174. }
  175. catch (e) {
  176. if (e.message.includes("The database connection is not open")) {
  177. // this often happens on killing the app which puts these alerts in front of user
  178. // in these cases error should be simply ignored.
  179. console.log(e.message);
  180. return null
  181. }
  182. throw e;
  183. }
  184. const milliseconds = Date.now() - startTimestamp;
  185. if (milliseconds >= 20) {
  186. if (query.includes("WITH RECURSIVE")) {
  187. log.info(`Slow recursive query took ${milliseconds}ms.`);
  188. }
  189. else {
  190. log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`);
  191. }
  192. }
  193. return result;
  194. }
  195. function transactional(func) {
  196. try {
  197. const ret = dbConnection.transaction(func).deferred();
  198. if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released)
  199. require('./ws').sendTransactionEntityChangesToAllClients();
  200. }
  201. return ret;
  202. }
  203. catch (e) {
  204. const entityChanges = cls.getAndClearEntityChangeIds();
  205. if (entityChanges.length > 0) {
  206. log.info("Transaction rollback dirtied the becca, forcing reload.");
  207. require('../becca/becca_loader').load();
  208. }
  209. throw e;
  210. }
  211. }
  212. function fillParamList(paramIds, truncate = true) {
  213. if (paramIds.length === 0) {
  214. return;
  215. }
  216. if (truncate) {
  217. execute("DELETE FROM param_list");
  218. }
  219. paramIds = Array.from(new Set(paramIds));
  220. if (paramIds.length > 30000) {
  221. fillParamList(paramIds.slice(30000), false);
  222. paramIds = paramIds.slice(0, 30000);
  223. }
  224. // doing it manually to avoid this showing up on the sloq query list
  225. const s = stmt(`INSERT INTO param_list VALUES ` + paramIds.map(paramId => `(?)`).join(','), paramIds);
  226. s.run(paramIds);
  227. }
  228. module.exports = {
  229. dbConnection,
  230. insert,
  231. replace,
  232. /**
  233. * Get single value from the given query - first column from first returned row.
  234. *
  235. * @method
  236. * @param {string} query - SQL query with ? used as parameter placeholder
  237. * @param {object[]} [params] - array of params if needed
  238. * @return [object] - single value
  239. */
  240. getValue,
  241. /**
  242. * Get first returned row.
  243. *
  244. * @method
  245. * @param {string} query - SQL query with ? used as parameter placeholder
  246. * @param {object[]} [params] - array of params if needed
  247. * @return {object} - map of column name to column value
  248. */
  249. getRow,
  250. getRowOrNull,
  251. /**
  252. * Get all returned rows.
  253. *
  254. * @method
  255. * @param {string} query - SQL query with ? used as parameter placeholder
  256. * @param {object[]} [params] - array of params if needed
  257. * @return {object[]} - array of all rows, each row is a map of column name to column value
  258. */
  259. getRows,
  260. getRawRows,
  261. iterateRows,
  262. getManyRows,
  263. /**
  264. * Get a map of first column mapping to second column.
  265. *
  266. * @method
  267. * @param {string} query - SQL query with ? used as parameter placeholder
  268. * @param {object[]} [params] - array of params if needed
  269. * @return {object} - map of first column to second column
  270. */
  271. getMap,
  272. /**
  273. * Get a first column in an array.
  274. *
  275. * @method
  276. * @param {string} query - SQL query with ? used as parameter placeholder
  277. * @param {object[]} [params] - array of params if needed
  278. * @return {object[]} - array of first column of all returned rows
  279. */
  280. getColumn,
  281. /**
  282. * Execute SQL
  283. *
  284. * @method
  285. * @param {string} query - SQL query with ? used as parameter placeholder
  286. * @param {object[]} [params] - array of params if needed
  287. */
  288. execute,
  289. executeWithoutTransaction,
  290. executeMany,
  291. executeScript,
  292. transactional,
  293. upsert,
  294. fillParamList
  295. };
  296. </code></pre>
  297. </article>
  298. </section>
  299. </div>
  300. <nav>
  301. <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="ApiToken.html">ApiToken</a></li><li><a href="Attribute.html">Attribute</a></li><li><a href="BackendScriptApi.html">BackendScriptApi</a></li><li><a href="Branch.html">Branch</a></li><li><a href="Note.html">Note</a></li><li><a href="NoteRevision.html">NoteRevision</a></li><li><a href="Option.html">Option</a></li><li><a href="RecentNote.html">RecentNote</a></li></ul><h3><a href="global.html">Global</a></h3>
  302. </nav>
  303. <br class="clear">
  304. <footer>
  305. Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a>
  306. </footer>
  307. <script> prettyPrint(); </script>
  308. <script src="scripts/linenumber.js"> </script>
  309. </body>
  310. </html>