ID photo of Ciro Santilli taken in 2013 right eyeCiro Santilli OurBigBook logoOurBigBook.com  Sponsor 中国独裁统治 China Dictatorship 新疆改造中心、六四事件、法轮功、郝海东、709大抓捕、2015巴拿马文件 邓家贵、低端人口、西藏骚乱
nodejs/sequelize/common.js
const assert = require('assert')
const path = require('path');

const { Sequelize } = require('sequelize');

function arrayEqual(a1, a2) {
  return a1.length === a2.length && a1.every((v, i) => v === a2[i])
}

function assertEqual(rows, rowsExpect) {
  if (assertEqual.typecast === undefined) {
    assertEqual.typecast = {}
  }
  assert.strictEqual(rows.length, rowsExpect.length)
  for (let i = 0; i < rows.length; i++) {
    let row = rows[i]
    let rowExpect = rowsExpect[i]
    for (let key in rowExpect) {
      // raw queries return raw Object without the .get method.
      // But when we have .get, use it, because certain queries require it.
      let rowval = typeof row.get === 'function' ? row.get(key) : row[key]
      const cur_typecast = assertEqual.typecast[key]
      if (cur_typecast) {
        rowval = cur_typecast(rowval)
      }
      let equal
      const expect = rowExpect[key]
      if (Array.isArray(rowval)) {
        equal = arrayEqual(rowval, expect)
      } else {
        equal = rowval === expect
      }
      if (!equal) {
        throw new Error(`assert failed: row ${i} column "${key}" actual "${rowval}" !== expect "${expect}"`)
      }
    }
  }
}
exports.assertEqual = assertEqual

async function drop(sequelize, table) {
  if (sequelize.options.dialect === 'postgres') {
    // We need CASCADE because we use a single database for all runs,
    // and some tables have conflicting names across tests.
    await sequelize.query(`DROP TABLE IF EXISTS "${table}" CASCADE`)
  } else {
    await sequelize.query(`DROP TABLE IF EXISTS "${table}"`)
  }
}
exports.drop = drop

function sequelize(filename, dialect, opts) {
  if (dialect === undefined) {
    dialect = 'l'
  }
  opts = Object.assign({
    logQueryParameters: true,
    // https://stackoverflow.com/questions/52260934/how-to-measure-query-execution-time-in-seqilize
    benchmark: true,
  }, opts)
  if (dialect === 'l') {
    const pathParse = path.parse(filename)
    return new Sequelize(Object.assign({
      dialect: 'sqlite',
      storage: path.join(pathParse.dir, pathParse.name) + '.sqlite'
    }, opts));
  } else if (dialect === 'p') {
    // To use the URI syntax, we need an explcit username and password.
    // But the second constructor works with peer authentication.
    // https://stackoverflow.com/questions/46207155/sequelize-and-peer-authentication-for-postgres
    //
    // Fails:
    //
    //const sequelize = new Sequelize('postgres://user:password@localhost:5432/lkmc-nodejs')
    //
    // Works with peer authentication: Before running this:
    // * ensure that you can use peer authentication without password
    //   from the command line, i.e. `psql` works
    // * create the database for our test:
    //   ``
    //   createdb lkmc-nodejs
    return new Sequelize('tmp', undefined, undefined, Object.assign({
      dialect: 'postgres',
      host: '/var/run/postgresql',
    }, opts));
  } else {
    throw new Error(`Unknown dialect: "${dialect}"`)
  }
}
exports.sequelize = sequelize

// Get n sequelize instances for parallel tests.
// Each one prepends its index to every log message.
function sequelizes(n, filename, dialect, opts={}) {
  return Array.from(
    Array(n).keys(),
    i => {
      let opts2 = Object.assign({}, opts)
      if (!('logging' in opts)) {
        opts2.logging = s => console.log(`${i}: ${s}`)
      }
      return sequelize(
        filename,
        dialect,
        opts2,
      )
    }
  )
}
exports.sequelizes = sequelizes

async function transaction(sequelize, isolation, cb) {
  let done = false
  if (isolation === undefined) {
    isolation = 'SERIALIZABLE'
  }
  while (!done) {
    if (isolation !== 'NONE') {
      if (sequelize.options.dialect === 'sqlite') {
        await sequelize.query(`BEGIN TRANSACTION`)
      } else {
        await sequelize.query(`BEGIN TRANSACTION ISOLATION LEVEL ${isolation}`)
      }
    }
    try {
      await cb(sequelize)
      if (isolation !== 'NONE') {
        await sequelize.query(`COMMIT`)
      }
      done = true;
    } catch (e) {
      if (
        sequelize.options.dialect === 'postgres' &&
        e instanceof Sequelize.DatabaseError &&
        // This can happen randomly, and we have to re-run the transaction:
        // - could not serialize access due to read/write dependencies among transactions
        // - could not serialize access due to concurrent update
        // https://www.postgresql.org/docs/13/errcodes-appendix.html
        e.original.code === '40001'
      ) {
        console.error(typeof(e));
        console.error(Sequelize.DatabaseError);
        console.error();
        if (isolation !== 'NONE') {
          await sequelize.query(`ROLLBACK`)
          // COMMIT would also work here in PostgreSQL it seems, when it enters error state it ignores
          // everything until the transaction finishes, and the COMMIT then becomes as ROLLBACK:
          // https://stackoverflow.com/questions/27245101/why-should-we-use-rollback-in-sql-explicitly/27245234#27245234
          // but can't find any clear docs on it, this one:
          // https://stackoverflow.com/questions/48277519/how-to-use-commit-and-rollback-in-a-postgresql-function/48277708#48277708
          // points to some possible docs, but not very direct.
          // await sequelize.query(`COMMIT`)
        }
      } else {
        // Error that we don't know how to handle.
        throw e;
      }
    }
  }
}
exports.transaction = transaction