OurBigBook logoOurBigBook Docs OurBigBook logoOurBigBook.comSite Source code
web/migrations/21000101000067-user-cache-listed-article-count.js
const { sequelizeCreateTriggerUpdateCount } = require('ourbigbook/nodejs_webpack_safe')

module.exports = {
  up: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction(async transaction => {
    const sequelize = queryInterface.sequelize
    // Adding the column locks User until commit. Lock Article first to match
    // article writers' lock order and keep the backfill consistent with writes.
    if (sequelize.getDialect() === 'postgres') {
      await sequelize.query('LOCK TABLE "Article" IN SHARE ROW EXCLUSIVE MODE', { transaction })
    }
    await queryInterface.addColumn('User', 'listedArticleCount', {
      type: Sequelize.DataTypes.INTEGER, allowNull: false, defaultValue: 0,
    }, { transaction })
    await sequelizeCreateTriggerUpdateCount(sequelize, { tableName: 'User' }, { tableName: 'Article' },
      'listedArticleCount', 'authorId', { countBooleanField: 'list', nameExtra: 'user_listed_article_count', transaction })
    await sequelize.query(`UPDATE "User" SET "listedArticleCount" = (
      SELECT CAST(COUNT(*) AS INTEGER) FROM "Article"
      WHERE "Article"."authorId" = "User"."id" AND "Article"."list" = ${sequelize.escape(true)}
    )`, { transaction })
  }),
  down: async queryInterface => {
    const sequelize = queryInterface.sequelize
    const sqlite = sequelize.getDialect() === 'sqlite'
    if (sqlite) await sequelize.query('PRAGMA foreign_keys = OFF')
    try {
      await sequelize.transaction(async transaction => {
        if (!sqlite) {
          await sequelize.query('LOCK TABLE "Article" IN SHARE ROW EXCLUSIVE MODE', { transaction })
        }
        for (const operation of ['insert', 'delete', 'update']) {
          const name = `Article_${operation}_user_listed_article_count`
          if (!sqlite) {
            await sequelize.query(`DROP TRIGGER IF EXISTS ${name} ON "Article"`, { transaction })
            await sequelize.query(`DROP FUNCTION IF EXISTS "${name}_fn"()`, { transaction })
          } else {
            await sequelize.query(`DROP TRIGGER IF EXISTS "${name}"`, { transaction })
          }
        }
        // SQLite rebuilds User when dropping a column. Preserve its indexes and
        // triggers; the new counter's triggers on Article were removed above.
        const [schema] = sqlite ? await sequelize.query(
          `SELECT sql FROM sqlite_master WHERE tbl_name = 'User' AND type IN ('index', 'trigger') AND sql IS NOT NULL`,
          { transaction },
        ) : [[]]
        if (sqlite) {
          // Sequelize's SQLite describeTable omits UNIQUE constraints. Preserve
          // the username/email constraints while rebuilding this parent table.
          const fields = await queryInterface.describeTable('User', { transaction })
          delete fields.listedArticleCount
          for (const index of await queryInterface.showIndex('User', { transaction })) {
            if (index.unique && index.name.startsWith('sqlite_autoindex_')) {
              if (index.fields.length !== 1) throw new Error('Cannot rebuild User with a composite UNIQUE constraint')
              fields[index.fields[0].attribute].unique = true
            }
          }
          const sql = queryInterface.queryGenerator.removeColumnQuery('User', fields)
          for (const statement of sql.split(';').filter(Boolean)) {
            await sequelize.query(statement, { transaction, raw: true })
          }
        } else {
          await queryInterface.removeColumn('User', 'listedArticleCount', { transaction })
        }
        for (const { sql } of schema) await sequelize.query(sql, { transaction })
      })
    } finally {
      if (sqlite) await sequelize.query('PRAGMA foreign_keys = ON')
    }
  },
}