SqliteConnectionPool.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. //TODO: удалить модуль в 2023г
  2. const sqlite3 = require('sqlite3');
  3. const sqlite = require('sqlite');
  4. const SQL = require('sql-template-strings');
  5. class SqliteConnectionPool {
  6. constructor() {
  7. this.closed = true;
  8. }
  9. async open(poolConfig, dbFileName) {
  10. const connCount = poolConfig.connCount || 1;
  11. const busyTimeout = poolConfig.busyTimeout || 60*1000;
  12. const cacheSize = poolConfig.cacheSize || 2000;
  13. this.dbFileName = dbFileName;
  14. this.connections = [];
  15. this.freed = new Set();
  16. this.waitingQueue = [];
  17. for (let i = 0; i < connCount; i++) {
  18. let client = await sqlite.open({
  19. filename: dbFileName,
  20. driver: sqlite3.Database
  21. });
  22. client.configure('busyTimeout', busyTimeout); //ms
  23. await client.exec(`PRAGMA cache_size = ${cacheSize}`);
  24. client.ret = () => {
  25. this.freed.add(i);
  26. if (this.waitingQueue.length) {
  27. this.waitingQueue.shift().onFreed(i);
  28. }
  29. };
  30. this.freed.add(i);
  31. this.connections[i] = client;
  32. }
  33. this.closed = false;
  34. }
  35. get() {
  36. return new Promise((resolve) => {
  37. if (this.closed)
  38. throw new Error('Connection pool closed');
  39. const freeConnIndex = this.freed.values().next().value;
  40. if (freeConnIndex !== undefined) {
  41. this.freed.delete(freeConnIndex);
  42. resolve(this.connections[freeConnIndex]);
  43. return;
  44. }
  45. this.waitingQueue.push({
  46. onFreed: (connIndex) => {
  47. this.freed.delete(connIndex);
  48. resolve(this.connections[connIndex]);
  49. },
  50. });
  51. });
  52. }
  53. async run(query) {
  54. const dbh = await this.get();
  55. try {
  56. let result = await dbh.run(query);
  57. dbh.ret();
  58. return result;
  59. } catch (e) {
  60. dbh.ret();
  61. throw e;
  62. }
  63. }
  64. async all(query) {
  65. const dbh = await this.get();
  66. try {
  67. let result = await dbh.all(query);
  68. dbh.ret();
  69. return result;
  70. } catch (e) {
  71. dbh.ret();
  72. throw e;
  73. }
  74. }
  75. async exec(query) {
  76. const dbh = await this.get();
  77. try {
  78. let result = await dbh.exec(query);
  79. dbh.ret();
  80. return result;
  81. } catch (e) {
  82. dbh.ret();
  83. throw e;
  84. }
  85. }
  86. async close() {
  87. for (let i = 0; i < this.connections.length; i++) {
  88. await this.connections[i].close();
  89. }
  90. this.closed = true;
  91. }
  92. // Modified from node-sqlite/.../src/Database.js
  93. async migrate(migs, table, force) {
  94. const migrations = migs.sort((a, b) => Math.sign(a.id - b.id));
  95. if (!migrations.length) {
  96. throw new Error('No migration data');
  97. }
  98. migrations.map(migration => {
  99. const data = migration.data;
  100. const [up, down] = data.split(/^--\s+?down\b/mi);
  101. if (!down) {
  102. const message = `The ${migration.filename} file does not contain '-- Down' separator.`;
  103. throw new Error(message);
  104. } else {
  105. /* eslint-disable no-param-reassign */
  106. migration.up = up.replace(/^-- .*?$/gm, '').trim();// Remove comments
  107. migration.down = down.trim(); // and trim whitespaces
  108. }
  109. });
  110. // Create a database table for migrations meta data if it doesn't exist
  111. await this.run(`CREATE TABLE IF NOT EXISTS "${table}" (
  112. id INTEGER PRIMARY KEY,
  113. name TEXT NOT NULL,
  114. up TEXT NOT NULL,
  115. down TEXT NOT NULL
  116. )`);
  117. // Get the list of already applied migrations
  118. let dbMigrations = await this.all(
  119. `SELECT id, name, up, down FROM "${table}" ORDER BY id ASC`,
  120. );
  121. // Undo migrations that exist only in the database but not in migs,
  122. // also undo the last migration if the `force` option was set to `last`.
  123. const lastMigration = migrations[migrations.length - 1];
  124. for (const migration of dbMigrations.slice().sort((a, b) => Math.sign(b.id - a.id))) {
  125. if (!migrations.some(x => x.id === migration.id) ||
  126. (force === 'last' && migration.id === lastMigration.id)) {
  127. const dbh = await this.get();
  128. await dbh.run('BEGIN');
  129. try {
  130. await dbh.exec(migration.down);
  131. await dbh.run(SQL`DELETE FROM "`.append(table).append(SQL`" WHERE id = ${migration.id}`));
  132. await dbh.run('COMMIT');
  133. dbMigrations = dbMigrations.filter(x => x.id !== migration.id);
  134. } catch (err) {
  135. await dbh.run('ROLLBACK');
  136. throw err;
  137. } finally {
  138. dbh.ret();
  139. }
  140. } else {
  141. break;
  142. }
  143. }
  144. // Apply pending migrations
  145. let applied = [];
  146. const lastMigrationId = dbMigrations.length ? dbMigrations[dbMigrations.length - 1].id : 0;
  147. for (const migration of migrations) {
  148. if (migration.id > lastMigrationId) {
  149. const dbh = await this.get();
  150. await dbh.run('BEGIN');
  151. try {
  152. await dbh.exec(migration.up);
  153. await dbh.run(SQL`INSERT INTO "`.append(table).append(
  154. SQL`" (id, name, up, down) VALUES (${migration.id}, ${migration.name}, ${migration.up}, ${migration.down})`)
  155. );
  156. await dbh.run('COMMIT');
  157. applied.push(migration.id);
  158. } catch (err) {
  159. await dbh.run('ROLLBACK');
  160. throw err;
  161. } finally {
  162. dbh.ret();
  163. }
  164. }
  165. }
  166. return applied;
  167. }
  168. }
  169. module.exports = SqliteConnectionPool;