浏览代码

Добавляем миграции в БД sqlite

Book Pauk 6 年之前
父节点
当前提交
5122cda6db

+ 0 - 90
server/core/SqliteConnectionPool.js

@@ -1,90 +0,0 @@
-const utils = require('./utils');
-const sqlite = require('sqlite');
-
-const waitingDelay = 100; //ms
-
-class SqliteConnectionPool {
-    constructor() {
-        this.closed = true;
-    }
-
-    async open(connCount, dbFileName) {
-        if (!Number.isInteger(connCount) || connCount <= 0)
-            return;
-        this.connections = [];
-        this.taken = new Set();
-        this.freed = new Set();
-
-        for (let i = 0; i < connCount; i++) {
-            let client = await sqlite.open(dbFileName);
-            client.configure('busyTimeout', 10000); //ms
-
-            client.ret = () => {
-                this.taken.delete(i);
-                this.freed.add(i);
-            };
-
-            this.freed.add(i);
-            this.connections[i] = client;
-        }
-        this.closed = false;
-    }
-
-    _setImmediate() {
-        return new Promise((resolve) => {
-            setImmediate(() => {
-                return resolve();
-            });
-        });
-    }
-
-    async get() {
-        if (this.closed)
-            return;
-
-        let freeConnIndex = this.freed.values().next().value;
-        if (freeConnIndex == null) {
-            if (waitingDelay)
-                await utils.sleep(waitingDelay);
-            return await this._setImmediate().then(() => this.get());
-        }
-
-        this.freed.delete(freeConnIndex);
-        this.taken.add(freeConnIndex);
-
-        return this.connections[freeConnIndex];
-    }
-
-    async run(query) {
-        const dbh = await this.get();
-        try {
-            let result = await dbh.run(query);
-            dbh.ret();
-            return result;
-        } catch (e) {
-            dbh.ret();
-            throw e;
-        }
-    }
-
-    async all(query) {
-        const dbh = await this.get();
-        try {
-            let result = await dbh.all(query);
-            dbh.ret();
-            return result;
-        } catch (e) {
-            dbh.ret();
-            throw e;
-        }
-    }
-
-    async close() {
-        for (let i = 0; i < this.connections.length; i++) {
-            await this.connections[i].close();
-        }
-        this.closed = true;
-    }
-}
-
-module.exports = SqliteConnectionPool;

+ 182 - 0
server/db/SqliteConnectionPool.js

@@ -0,0 +1,182 @@
+const sqlite = require('sqlite');
+const SQL = require('sql-template-strings');
+
+const utils = require('../core/utils');
+
+const waitingDelay = 100; //ms
+
+class SqliteConnectionPool {
+    constructor() {
+        this.closed = true;
+    }
+
+    async open(connCount, dbFileName) {
+        if (!Number.isInteger(connCount) || connCount <= 0)
+            return;
+        this.connections = [];
+        this.taken = new Set();
+        this.freed = new Set();
+
+        for (let i = 0; i < connCount; i++) {
+            let client = await sqlite.open(dbFileName);
+            client.configure('busyTimeout', 10000); //ms
+
+            client.ret = () => {
+                this.taken.delete(i);
+                this.freed.add(i);
+            };
+
+            this.freed.add(i);
+            this.connections[i] = client;
+        }
+        this.closed = false;
+    }
+
+    _setImmediate() {
+        return new Promise((resolve) => {
+            setImmediate(() => {
+                return resolve();
+            });
+        });
+    }
+
+    async get() {
+        if (this.closed)
+            return;
+
+        let freeConnIndex = this.freed.values().next().value;
+        if (freeConnIndex == null) {
+            if (waitingDelay)
+                await utils.sleep(waitingDelay);
+            return await this._setImmediate().then(() => this.get());
+        }
+
+        this.freed.delete(freeConnIndex);
+        this.taken.add(freeConnIndex);
+
+        return this.connections[freeConnIndex];
+    }
+
+    async run(query) {
+        const dbh = await this.get();
+        try {
+            let result = await dbh.run(query);
+            dbh.ret();
+            return result;
+        } catch (e) {
+            dbh.ret();
+            throw e;
+        }
+    }
+
+    async all(query) {
+        const dbh = await this.get();
+        try {
+            let result = await dbh.all(query);
+            dbh.ret();
+            return result;
+        } catch (e) {
+            dbh.ret();
+            throw e;
+        }
+    }
+
+    async exec(query) {
+        const dbh = await this.get();
+        try {
+            let result = await dbh.exec(query);
+            dbh.ret();
+            return result;
+        } catch (e) {
+            dbh.ret();
+            throw e;
+        }
+    }
+
+    async close() {
+        for (let i = 0; i < this.connections.length; i++) {
+            await this.connections[i].close();
+        }
+        this.closed = true;
+    }
+
+     // Modified from node-sqlite/.../src/Database.js
+    async migrate(migs, table, force) {
+        const migrations = migs.sort((a, b) => Math.sign(a.id - b.id));
+
+        if (!migrations.length) {
+            throw new Error('No migration data');
+        }
+
+        migrations.map(migration => {
+            const data = migration.data;
+            const [up, down] = data.split(/^--\s+?down\b/mi);
+            if (!down) {
+                const message = `The ${migration.filename} file does not contain '-- Down' separator.`;
+                throw new Error(message);
+            } else {
+                /* eslint-disable no-param-reassign */
+                migration.up = up.replace(/^-- .*?$/gm, '').trim();// Remove comments
+                migration.down = down.trim(); // and trim whitespaces
+            }
+        });
+
+        // Create a database table for migrations meta data if it doesn't exist
+        await this.run(`CREATE TABLE IF NOT EXISTS "${table}" (
+    id   INTEGER PRIMARY KEY,
+    name TEXT    NOT NULL,
+    up   TEXT    NOT NULL,
+    down TEXT    NOT NULL
+)`);
+
+        // Get the list of already applied migrations
+        let dbMigrations = await this.all(
+            `SELECT id, name, up, down FROM "${table}" ORDER BY id ASC`,
+        );
+
+        // Undo migrations that exist only in the database but not in migs,
+        // also undo the last migration if the `force` option was set to `last`.
+        const lastMigration = migrations[migrations.length - 1];
+        for (const migration of dbMigrations.slice().sort((a, b) => Math.sign(b.id - a.id))) {
+            if (!migrations.some(x => x.id === migration.id) ||
+                (force === 'last' && migration.id === lastMigration.id)) {
+                await this.run('BEGIN');
+                try {
+                    await this.exec(migration.down);
+                    await this.run(SQL`DELETE FROM "`.append(table).append(SQL`" WHERE id = ${migration.id}`));
+                    await this.run('COMMIT');
+                    dbMigrations = dbMigrations.filter(x => x.id !== migration.id);
+                } catch (err) {
+                    await this.run('ROLLBACK');
+                    throw err;
+                }
+            } else {
+                break;
+            }
+        }
+
+        // Apply pending migrations
+        let applied = [];
+        const lastMigrationId = dbMigrations.length ? dbMigrations[dbMigrations.length - 1].id : 0;
+        for (const migration of migrations) {
+            if (migration.id > lastMigrationId) {
+                await this.run('BEGIN');
+                try {
+                    await this.exec(migration.up);
+                    await this.run(SQL`INSERT INTO "`.append(table).append(
+                        SQL`" (id, name, up, down) VALUES (${migration.id}, ${migration.name}, ${migration.up}, ${migration.down})`)
+                    );
+                    await this.run('COMMIT');
+                    applied.push(migration.id);
+                } catch (err) {
+                    await this.run('ROLLBACK');
+                    throw err;
+                }
+            }
+        }
+
+        return applied;
+    }
+}
+
+module.exports = SqliteConnectionPool;

+ 17 - 0
server/core/connManager.js → server/db/connManager.js

@@ -1,4 +1,10 @@
 const SqliteConnectionPool = require('./SqliteConnectionPool');
 const SqliteConnectionPool = require('./SqliteConnectionPool');
+const log = require('../core/getLogger').getLog();
+
+const migrations = {
+    'app': require('./migrations/app'),
+    'readerStorage': require('./migrations/readerStorage'),
+};
 
 
 class ConnManager {
 class ConnManager {
     constructor() {
     constructor() {
@@ -7,11 +13,22 @@ class ConnManager {
 
 
     async init(config) {
     async init(config) {
         this.config = config;
         this.config = config;
+
+        const force = (config.branch == 'development' ? 'last' : null);
+
         for (const poolConfig of this.config.db) {
         for (const poolConfig of this.config.db) {
             const dbFileName = this.config.dataDir + '/' + poolConfig.fileName;
             const dbFileName = this.config.dataDir + '/' + poolConfig.fileName;
             const connPool = new SqliteConnectionPool();
             const connPool = new SqliteConnectionPool();
             await connPool.open(poolConfig.connCount, dbFileName);
             await connPool.open(poolConfig.connCount, dbFileName);
 
 
+            log(`Opened database "${poolConfig.poolName}"`);
+            //миграции
+            const migs = migrations[poolConfig.poolName];
+            if (migs && migs.data.length) {
+                const applied = await connPool.migrate(migs.data, migs.table, force);
+                log(`Applied ${applied} migrations to "${poolConfig.poolName}"`);
+            }
+
             this._pool[poolConfig.poolName] = connPool;
             this._pool[poolConfig.poolName] = connPool;
         }
         }
     }
     }

+ 5 - 0
server/db/migrations/app/index.js

@@ -0,0 +1,5 @@
+module.exports = {
+    table: 'migration1',
+    data: [
+    ]
+}

+ 5 - 0
server/db/migrations/readerStorage/index.js

@@ -0,0 +1,5 @@
+module.exports = {
+    table: 'migration1',
+    data: [
+    ]
+}

+ 1 - 3
server/index.js

@@ -11,7 +11,7 @@ const path = require('path');
 const express = require('express');
 const express = require('express');
 const compression = require('compression');
 const compression = require('compression');
 
 
-const connManager = require('./core/connManager');
+const connManager = require('./db/connManager');
 
 
 async function init() {
 async function init() {
     await fs.ensureDir(config.dataDir);
     await fs.ensureDir(config.dataDir);
@@ -35,9 +35,7 @@ async function main() {
     log('Initializing');
     log('Initializing');
     await init();
     await init();
 
 
-    log('Opening databases');
     await connManager.init(config);
     await connManager.init(config);
-    log(`Opened databases: ${Object.keys(connManager.pool).join(', ')}`);
 
 
     //servers
     //servers
     for (let server of config.servers) {
     for (let server of config.servers) {