SqliteConnectionPool.js 6.0 KB

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