services_sql.js.html 10 KB

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