SqliteConnectionPool.js 5.7 KB

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