mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-11-01 05:21:32 +08:00 
			
		
		
		
	
		
			
	
	
		
			423 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
		
		
			
		
	
	
			423 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
|   | <!DOCTYPE html> | ||
|  | <html lang="en"> | ||
|  | <head> | ||
|  |     <meta charset="utf-8"> | ||
|  |     <title>JSDoc: Source: services/sql.js</title> | ||
|  | 
 | ||
|  |     <script src="scripts/prettify/prettify.js"> </script> | ||
|  |     <script src="scripts/prettify/lang-css.js"> </script> | ||
|  |     <!--[if lt IE 9]>
 | ||
|  |       <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> | ||
|  |     <![endif]--> | ||
|  |     <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> | ||
|  |     <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> | ||
|  | </head> | ||
|  | 
 | ||
|  | <body> | ||
|  | 
 | ||
|  | <div id="main"> | ||
|  | 
 | ||
|  |     <h1 class="page-title">Source: services/sql.js</h1> | ||
|  | 
 | ||
|  |      | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  |      | ||
|  |     <section> | ||
|  |         <article> | ||
|  |             <pre class="prettyprint source linenums"><code>"use strict"; | ||
|  | 
 | ||
|  | /** | ||
|  |  * @module sql | ||
|  |  */ | ||
|  | 
 | ||
|  | const log = require('./log'); | ||
|  | const Database = require('better-sqlite3'); | ||
|  | const dataDir = require('./data_dir'); | ||
|  | const cls = require('./cls'); | ||
|  | const fs = require("fs-extra"); | ||
|  | 
 | ||
|  | const dbConnection = new Database(dataDir.DOCUMENT_PATH); | ||
|  | dbConnection.pragma('journal_mode = WAL'); | ||
|  | 
 | ||
|  | const LOG_ALL_QUERIES = false; | ||
|  | 
 | ||
|  | [`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => { | ||
|  |     process.on(eventType, () => { | ||
|  |         if (dbConnection) { | ||
|  |             // closing connection is especially important to fold -wal file into the main DB file | ||
|  |             // (see https://sqlite.org/tempfiles.html for details) | ||
|  |             dbConnection.close(); | ||
|  |         } | ||
|  |     }); | ||
|  | }); | ||
|  | 
 | ||
|  | function insert(tableName, rec, replace = false) { | ||
|  |     const keys = Object.keys(rec); | ||
|  |     if (keys.length === 0) { | ||
|  |         log.error(`Can't insert empty object into table ${tableName}`); | ||
|  |         return; | ||
|  |     } | ||
|  | 
 | ||
|  |     const columns = keys.join(", "); | ||
|  |     const questionMarks = keys.map(p => "?").join(", "); | ||
|  | 
 | ||
|  |     const query = `INSERT | ||
|  |     ${replace ? "OR REPLACE" : ""} INTO | ||
|  |     ${tableName} | ||
|  |     ( | ||
|  |     ${columns} | ||
|  |     ) | ||
|  |     VALUES (${questionMarks})`; | ||
|  | 
 | ||
|  |     const res = execute(query, Object.values(rec)); | ||
|  | 
 | ||
|  |     return res ? res.lastInsertRowid : null; | ||
|  | } | ||
|  | 
 | ||
|  | function replace(tableName, rec) { | ||
|  |     return insert(tableName, rec, true); | ||
|  | } | ||
|  | 
 | ||
|  | function upsert(tableName, primaryKey, rec) { | ||
|  |     const keys = Object.keys(rec); | ||
|  |     if (keys.length === 0) { | ||
|  |         log.error(`Can't upsert empty object into table ${tableName}`); | ||
|  |         return; | ||
|  |     } | ||
|  | 
 | ||
|  |     const columns = keys.join(", "); | ||
|  | 
 | ||
|  |     const questionMarks = keys.map(colName => `@${colName}`).join(", "); | ||
|  | 
 | ||
|  |     const updateMarks = keys.map(colName => `${colName} = @${colName}`).join(", "); | ||
|  | 
 | ||
|  |     const query = `INSERT INTO ${tableName} (${columns}) VALUES (${questionMarks})  | ||
|  |                    ON CONFLICT (${primaryKey}) DO UPDATE SET ${updateMarks}`; | ||
|  | 
 | ||
|  |     for (const idx in rec) { | ||
|  |         if (rec[idx] === true || rec[idx] === false) { | ||
|  |             rec[idx] = rec[idx] ? 1 : 0; | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     execute(query, rec); | ||
|  | } | ||
|  | 
 | ||
|  | const statementCache = {}; | ||
|  | 
 | ||
|  | function stmt(sql) { | ||
|  |     if (!(sql in statementCache)) { | ||
|  |         statementCache[sql] = dbConnection.prepare(sql); | ||
|  |     } | ||
|  | 
 | ||
|  |     return statementCache[sql]; | ||
|  | } | ||
|  | 
 | ||
|  | function getRow(query, params = []) { | ||
|  |     return wrap(query, s => s.get(params)); | ||
|  | } | ||
|  | 
 | ||
|  | function getRowOrNull(query, params = []) { | ||
|  |     const all = getRows(query, params); | ||
|  | 
 | ||
|  |     return all.length > 0 ? all[0] : null; | ||
|  | } | ||
|  | 
 | ||
|  | function getValue(query, params = []) { | ||
|  |     return wrap(query, s => s.pluck().get(params)); | ||
|  | } | ||
|  | 
 | ||
|  | // smaller values can result in better performance due to better usage of statement cache | ||
|  | const PARAM_LIMIT = 100; | ||
|  | 
 | ||
|  | function getManyRows(query, params) { | ||
|  |     let results = []; | ||
|  | 
 | ||
|  |     while (params.length > 0) { | ||
|  |         const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT)); | ||
|  |         params = params.slice(curParams.length); | ||
|  | 
 | ||
|  |         const curParamsObj = {}; | ||
|  | 
 | ||
|  |         let j = 1; | ||
|  |         for (const param of curParams) { | ||
|  |             curParamsObj['param' + j++] = param; | ||
|  |         } | ||
|  | 
 | ||
|  |         let i = 1; | ||
|  |         const questionMarks = curParams.map(() => ":param" + i++).join(","); | ||
|  |         const curQuery = query.replace(/\?\?\?/g, questionMarks); | ||
|  | 
 | ||
|  |         const statement = curParams.length === PARAM_LIMIT | ||
|  |             ? stmt(curQuery) | ||
|  |             : dbConnection.prepare(curQuery); | ||
|  | 
 | ||
|  |         const subResults = statement.all(curParamsObj); | ||
|  |         results = results.concat(subResults); | ||
|  |     } | ||
|  | 
 | ||
|  |     return results; | ||
|  | } | ||
|  | 
 | ||
|  | function getRows(query, params = []) { | ||
|  |     return wrap(query, s => s.all(params)); | ||
|  | } | ||
|  | 
 | ||
|  | function getRawRows(query, params = []) { | ||
|  |     return wrap(query, s => s.raw().all(params)); | ||
|  | } | ||
|  | 
 | ||
|  | function iterateRows(query, params = []) { | ||
|  |     if (LOG_ALL_QUERIES) { | ||
|  |         console.log(query); | ||
|  |     } | ||
|  | 
 | ||
|  |     return stmt(query).iterate(params); | ||
|  | } | ||
|  | 
 | ||
|  | function getMap(query, params = []) { | ||
|  |     const map = {}; | ||
|  |     const results = getRawRows(query, params); | ||
|  | 
 | ||
|  |     for (const row of results) { | ||
|  |         map[row[0]] = row[1]; | ||
|  |     } | ||
|  | 
 | ||
|  |     return map; | ||
|  | } | ||
|  | 
 | ||
|  | function getColumn(query, params = []) { | ||
|  |     return wrap(query, s => s.pluck().all(params)); | ||
|  | } | ||
|  | 
 | ||
|  | function execute(query, params = []) { | ||
|  |     return wrap(query, s => s.run(params)); | ||
|  | } | ||
|  | 
 | ||
|  | function executeMany(query, params) { | ||
|  |     if (LOG_ALL_QUERIES) { | ||
|  |         console.log(query); | ||
|  |     } | ||
|  | 
 | ||
|  |     while (params.length > 0) { | ||
|  |         const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT)); | ||
|  |         params = params.slice(curParams.length); | ||
|  | 
 | ||
|  |         const curParamsObj = {}; | ||
|  | 
 | ||
|  |         let j = 1; | ||
|  |         for (const param of curParams) { | ||
|  |             curParamsObj['param' + j++] = param; | ||
|  |         } | ||
|  | 
 | ||
|  |         let i = 1; | ||
|  |         const questionMarks = curParams.map(() => ":param" + i++).join(","); | ||
|  |         const curQuery = query.replace(/\?\?\?/g, questionMarks); | ||
|  | 
 | ||
|  |         dbConnection.prepare(curQuery).run(curParamsObj); | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | function executeScript(query) { | ||
|  |     if (LOG_ALL_QUERIES) { | ||
|  |         console.log(query); | ||
|  |     } | ||
|  | 
 | ||
|  |     return dbConnection.exec(query); | ||
|  | } | ||
|  | 
 | ||
|  | function wrap(query, func) { | ||
|  |     const startTimestamp = Date.now(); | ||
|  |     let result; | ||
|  | 
 | ||
|  |     if (LOG_ALL_QUERIES) { | ||
|  |         console.log(query); | ||
|  |     } | ||
|  | 
 | ||
|  |     try { | ||
|  |         result = func(stmt(query)); | ||
|  |     } | ||
|  |     catch (e) { | ||
|  |         if (e.message.includes("The database connection is not open")) { | ||
|  |             // this often happens on killing the app which puts these alerts in front of user | ||
|  |             // in these cases error should be simply ignored. | ||
|  |             console.log(e.message); | ||
|  | 
 | ||
|  |             return null | ||
|  |         } | ||
|  | 
 | ||
|  |         throw e; | ||
|  |     } | ||
|  | 
 | ||
|  |     const milliseconds = Date.now() - startTimestamp; | ||
|  | 
 | ||
|  |     if (milliseconds >= 20) { | ||
|  |         if (query.includes("WITH RECURSIVE")) { | ||
|  |             log.info(`Slow recursive query took ${milliseconds}ms.`); | ||
|  |         } | ||
|  |         else { | ||
|  |             log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`); | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     return result; | ||
|  | } | ||
|  | 
 | ||
|  | function transactional(func) { | ||
|  |     try { | ||
|  |         const ret = dbConnection.transaction(func).deferred(); | ||
|  | 
 | ||
|  |         if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released) | ||
|  |             require('./ws').sendTransactionEntityChangesToAllClients(); | ||
|  |         } | ||
|  | 
 | ||
|  |         return ret; | ||
|  |     } | ||
|  |     catch (e) { | ||
|  |         const entityChangeIds = cls.getAndClearEntityChangeIds(); | ||
|  | 
 | ||
|  |         if (entityChangeIds.length > 0) { | ||
|  |             log.info("Transaction rollback dirtied the becca, forcing reload."); | ||
|  | 
 | ||
|  |             require('../becca/becca_loader').load(); | ||
|  |         } | ||
|  | 
 | ||
|  |         // the maxEntityChangeId has been incremented during failed transaction, need to recalculate | ||
|  |         require('./entity_changes').recalculateMaxEntityChangeId(); | ||
|  | 
 | ||
|  |         throw e; | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | function fillParamList(paramIds, truncate = true) { | ||
|  |     if (paramIds.length === 0) { | ||
|  |         return; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (truncate) { | ||
|  |         execute("DELETE FROM param_list"); | ||
|  |     } | ||
|  | 
 | ||
|  |     paramIds = Array.from(new Set(paramIds)); | ||
|  | 
 | ||
|  |     if (paramIds.length > 30000) { | ||
|  |         fillParamList(paramIds.slice(30000), false); | ||
|  | 
 | ||
|  |         paramIds = paramIds.slice(0, 30000); | ||
|  |     } | ||
|  | 
 | ||
|  |     // doing it manually to avoid this showing up on the sloq query list | ||
|  |     const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`, paramIds); | ||
|  | 
 | ||
|  |     s.run(paramIds); | ||
|  | } | ||
|  | 
 | ||
|  | async function copyDatabase(targetFilePath) { | ||
|  |     try { | ||
|  |         fs.unlinkSync(targetFilePath); | ||
|  |     } catch (e) { | ||
|  |     } // unlink throws exception if the file did not exist | ||
|  | 
 | ||
|  |     await dbConnection.backup(targetFilePath); | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = { | ||
|  |     dbConnection, | ||
|  |     insert, | ||
|  |     replace, | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Get single value from the given query - first column from first returned row. | ||
|  |      * | ||
|  |      * @method | ||
|  |      * @param {string} query - SQL query with ? used as parameter placeholder | ||
|  |      * @param {object[]} [params] - array of params if needed | ||
|  |      * @returns [object] - single value | ||
|  |      */ | ||
|  |     getValue, | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Get first returned row. | ||
|  |      * | ||
|  |      * @method | ||
|  |      * @param {string} query - SQL query with ? used as parameter placeholder | ||
|  |      * @param {object[]} [params] - array of params if needed | ||
|  |      * @returns {object} - map of column name to column value | ||
|  |      */ | ||
|  |     getRow, | ||
|  |     getRowOrNull, | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Get all returned rows. | ||
|  |      * | ||
|  |      * @method | ||
|  |      * @param {string} query - SQL query with ? used as parameter placeholder | ||
|  |      * @param {object[]} [params] - array of params if needed | ||
|  |      * @returns {object[]} - array of all rows, each row is a map of column name to column value | ||
|  |      */ | ||
|  |     getRows, | ||
|  |     getRawRows, | ||
|  |     iterateRows, | ||
|  |     getManyRows, | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Get a map of first column mapping to second column. | ||
|  |      * | ||
|  |      * @method | ||
|  |      * @param {string} query - SQL query with ? used as parameter placeholder | ||
|  |      * @param {object[]} [params] - array of params if needed | ||
|  |      * @returns {object} - map of first column to second column | ||
|  |      */ | ||
|  |     getMap, | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Get a first column in an array. | ||
|  |      * | ||
|  |      * @method | ||
|  |      * @param {string} query - SQL query with ? used as parameter placeholder | ||
|  |      * @param {object[]} [params] - array of params if needed | ||
|  |      * @returns {object[]} - array of first column of all returned rows | ||
|  |      */ | ||
|  |     getColumn, | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Execute SQL | ||
|  |      * | ||
|  |      * @method | ||
|  |      * @param {string} query - SQL query with ? used as parameter placeholder | ||
|  |      * @param {object[]} [params] - array of params if needed | ||
|  |      */ | ||
|  |     execute, | ||
|  |     executeMany, | ||
|  |     executeScript, | ||
|  |     transactional, | ||
|  |     upsert, | ||
|  |     fillParamList, | ||
|  |     copyDatabase | ||
|  | }; | ||
|  | </code></pre> | ||
|  |         </article> | ||
|  |     </section> | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | </div> | ||
|  | 
 | ||
|  | <nav> | ||
|  |     <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="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="BNoteRevision.html">BNoteRevision</a></li><li><a href="BOption.html">BOption</a></li><li><a href="BRecentNote.html">BRecentNote</a></li><li><a href="BackendScriptApi.html">BackendScriptApi</a></li></ul> | ||
|  | </nav> | ||
|  | 
 | ||
|  | <br class="clear"> | ||
|  | 
 | ||
|  | <footer> | ||
|  |     Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.1</a> | ||
|  | </footer> | ||
|  | 
 | ||
|  | <script> prettyPrint(); </script> | ||
|  | <script src="scripts/linenumber.js"> </script> | ||
|  | </body> | ||
|  | </html> |